diff --git a/eatery-service/src/main/java/com/drinkool/dtos/DtoMapper.java b/eatery-service/src/main/java/com/drinkool/dtos/DtoMapper.java index ea734ed..659bec5 100644 --- a/eatery-service/src/main/java/com/drinkool/dtos/DtoMapper.java +++ b/eatery-service/src/main/java/com/drinkool/dtos/DtoMapper.java @@ -1,6 +1,8 @@ package com.drinkool.dtos; import com.drinkool.entities.MenuItemEntity; +import com.drinkool.entities.ReviewEntity; +import com.drinkool.lib.dtos.SendReview; import org.mapstruct.BeanMapping; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; @@ -15,4 +17,6 @@ public interface DtoMapper { UpdateMenuItem input, @MappingTarget MenuItemEntity entity ); + + ReviewEntity toReviewEntity(SendReview source); } diff --git a/eatery-service/src/main/java/com/drinkool/entities/ReviewEntity.java b/eatery-service/src/main/java/com/drinkool/entities/ReviewEntity.java index 4477958..d563242 100644 --- a/eatery-service/src/main/java/com/drinkool/entities/ReviewEntity.java +++ b/eatery-service/src/main/java/com/drinkool/entities/ReviewEntity.java @@ -1,35 +1,34 @@ package com.drinkool.entities; -import com.drinkool.lib.models.ReviewModel; +import com.drinkool.interfaces.ReviewInterface; +import com.drinkool.lib.entities.BaseEntity; import jakarta.persistence.*; -import jakarta.transaction.Transactional; import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor @Table(name = "reviews") -public class ReviewEntity extends ReviewModel { +public class ReviewEntity extends BaseEntity implements ReviewInterface { public UUID reviewerId; public UUID eateryId; - private ReviewEntity(ReviewModel input) { - super(input); - } + public Integer rating; - @Transactional - public static ReviewEntity create( - UUID reviewerId, - UUID eateryId, - ReviewModel reviewInput - ) { - ReviewEntity review = new ReviewEntity(reviewInput); + public String comment; - review.reviewerId = reviewerId; - review.eateryId = eateryId; - - review.persist(); - - return review; + public void setRating(Integer score) { + if (score < 1 || score > 5) { + throw new IllegalArgumentException("InvalidRatingValue"); + } + this.rating = score; } } diff --git a/eatery-service/src/main/java/com/drinkool/services/ReviewService.java b/eatery-service/src/main/java/com/drinkool/services/ReviewService.java index 555ba74..397da26 100644 --- a/eatery-service/src/main/java/com/drinkool/services/ReviewService.java +++ b/eatery-service/src/main/java/com/drinkool/services/ReviewService.java @@ -1,20 +1,71 @@ package com.drinkool.services; +import com.drinkool.InternalValue; +import com.drinkool.Role; +import com.drinkool.Url; +import com.drinkool.dtos.DtoMapper; import com.drinkool.entities.ReviewEntity; +import com.drinkool.enums.SortBy; import com.drinkool.lib.dtos.SendReview; +import io.quarkus.panache.common.Sort; +import io.vertx.core.http.HttpServerRequest; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; import jakarta.transaction.Transactional; -import jakarta.ws.rs.*; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Response; +import java.util.UUID; -@Path("/api/review") +@Path(Url.Review) public class ReviewService { - @POST - @Path("/create") - @Transactional - public Response create(SendReview input) { - ReviewEntity.create(input.reviewerId, input.orderId, input.review); + @Inject + HttpServerRequest request; - return Response.status(201).build(); + @Inject + DtoMapper dtoMapper; + + @GET + @Transactional + @Path("/{eateryId}") + public Response getReviewsByEatery( + @PathParam("eateryId") UUID eateryId, + @QueryParam("sort") @DefaultValue("LATEST") SortBy sortBy + ) { + Sort sort = switch (sortBy) { + case LATEST -> Sort.descending("createDate"); + case OLDEST -> Sort.ascending("createDate"); + case HIGHEST -> Sort.descending("rating"); + case LOWEST -> Sort.ascending("rating"); + }; + + return Response.ok() + .entity(ReviewEntity.find("eateryId = ?1", sort, eateryId).list()) + .build(); + } + + @POST + @RolesAllowed(Role.Customer) + @Transactional + public Response receiveReview(SendReview input) { + UUID reviewerId = UUID.fromString(request.getHeader(InternalValue.userId)); + + try { + ReviewEntity entity = dtoMapper.toReviewEntity(input); + entity.setReviewerId(reviewerId); + + entity.persist(); + + return Response.status(Response.Status.CREATED).build(); + } catch (Exception e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(e.getMessage()) + .build(); + } } } diff --git a/eatery-service/src/test/java/com/drinkool/entities/ReviewEntityTest.java b/eatery-service/src/test/java/com/drinkool/entities/ReviewEntityTest.java index 9dedee1..cf83d28 100644 --- a/eatery-service/src/test/java/com/drinkool/entities/ReviewEntityTest.java +++ b/eatery-service/src/test/java/com/drinkool/entities/ReviewEntityTest.java @@ -1,32 +1,34 @@ package com.drinkool.entities; -import com.drinkool.lib.models.ReviewModel; import io.quarkus.test.junit.QuarkusTest; import jakarta.transaction.Transactional; import java.util.UUID; +import net.datafaker.Faker; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @QuarkusTest public class ReviewEntityTest { + private final Faker faker = new Faker(); + @Test @Transactional public void testCreateReview() { - // 1. Chuẩn bị dữ liệu (Arrange) UUID testReviewerId = UUID.randomUUID(); UUID testEateryId = UUID.randomUUID(); + String comment = faker.famousLastWords().lastWords(); + int rating = faker.random().nextInt(1, 5); - // Truyền trực tiếp rating và comment vào constructor của ReviewModel - ReviewModel inputModel = new ReviewModel(5, "Món ăn tuyệt vời!"); - - // 2. Thực thi (Act) - ReviewEntity savedReview = ReviewEntity.create( + ReviewEntity savedReview = new ReviewEntity( testReviewerId, testEateryId, - inputModel + rating, + comment ); + savedReview.persist(); + // 3. Kiểm chứng (Assert) Assertions.assertNotNull( savedReview, @@ -42,12 +44,8 @@ public class ReviewEntityTest { savedReview.eateryId, "Eatery ID không khớp" ); - Assertions.assertEquals(5, savedReview.rating, "Rating không khớp"); - Assertions.assertEquals( - "Món ăn tuyệt vời!", - savedReview.comment, - "Comment không khớp" - ); + Assertions.assertEquals(rating, savedReview.rating, "Rating không khớp"); + Assertions.assertEquals(comment, savedReview.comment, "Comment không khớp"); Assertions.assertNotNull( savedReview.id, diff --git a/eatery-service/src/test/java/com/drinkool/services/ReviewServiceTest.java b/eatery-service/src/test/java/com/drinkool/services/ReviewServiceTest.java index bdab888..ae893f9 100644 --- a/eatery-service/src/test/java/com/drinkool/services/ReviewServiceTest.java +++ b/eatery-service/src/test/java/com/drinkool/services/ReviewServiceTest.java @@ -1,43 +1,109 @@ package com.drinkool.services; import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; import static org.junit.jupiter.api.Assertions.*; +import com.drinkool.InternalValue; +import com.drinkool.Role; +import com.drinkool.Url; +import com.drinkool.entities.EateryEntity; +import com.drinkool.entities.MenuItemEntity; import com.drinkool.entities.ReviewEntity; import com.drinkool.lib.dtos.SendReview; -import com.drinkool.lib.models.ReviewModel; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; -import java.util.Random; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.core.Response; import java.util.UUID; +import net.datafaker.Faker; import org.junit.jupiter.api.*; @QuarkusTest public class ReviewServiceTest { + private final Faker faker = new Faker(); + + private UUID savedEateryId; + private String savedEateryName; + private UUID savedOwnerId = UUID.randomUUID(); + + @BeforeEach + @Transactional + void setup() { + ReviewEntity.deleteAll(); + MenuItemEntity.deleteAll(); + EateryEntity.deleteAll(); + + savedEateryName = faker.restaurant().name(); + + EateryEntity eatery = new EateryEntity(); + eatery = eatery.create(savedOwnerId, savedEateryName); + savedEateryId = eatery.id; + + for (int i = 0; i < 3; i++) { + ReviewEntity entity = new ReviewEntity( + UUID.randomUUID(), + savedEateryId, + faker.random().nextInt(1, 5), + faker.famousLastWords().lastWords() + ); + + entity.persist(); + } + } + @Test - void receiveReview() { - Random random = new Random(); - + void receiveReview_Success() { UUID reviewerId = UUID.randomUUID(); - UUID orderId = UUID.randomUUID(); - String comment = UUID.randomUUID().toString(); - int rating = random.nextInt(); + UUID eateryId = UUID.randomUUID(); + String comment = faker.famousLastWords().lastWords(); + int rating = faker.random().nextInt(1, 5); - SendReview input = new SendReview(); - input.reviewerId = reviewerId; - input.orderId = orderId; - ReviewModel reviewModel = new ReviewModel(rating, comment); - input.review = reviewModel; + SendReview input = new SendReview(reviewerId, eateryId, rating, comment); given() + .header(InternalValue.role, Role.Customer) + .header(InternalValue.userId, reviewerId) .contentType(ContentType.JSON) .body(input) .when() - .post("/api/review/create") + .post(Url.Review) .then() - .statusCode(201); + .statusCode(Response.Status.CREATED.getStatusCode()); assertEquals(1, ReviewEntity.find("reviewerId", reviewerId).count()); } + + @Test + void receiveReview_FailedDueToInvalidRating() { + UUID reviewerId = UUID.randomUUID(); + UUID eateryId = UUID.randomUUID(); + String comment = faker.famousLastWords().lastWords(); + int rating = faker.random().nextInt(6, 10); + + SendReview input = new SendReview(reviewerId, eateryId, rating, comment); + + given() + .header(InternalValue.role, Role.Customer) + .header(InternalValue.userId, UUID.randomUUID()) + .contentType(ContentType.JSON) + .body(input) + .when() + .post(Url.Review) + .then() + .contentType(ContentType.TEXT) + .body(is("InvalidRatingValue")) + .statusCode(Response.Status.BAD_REQUEST.getStatusCode()); + } + + @Test + public void testGetReviews_DefaultLatest() { + given() + .when() + .get(Url.Review + '/' + savedEateryId.toString()) + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .contentType(ContentType.JSON); + } } diff --git a/gateway-service/src/main/java/com/drinkool/controller/EateryController.java b/gateway-service/src/main/java/com/drinkool/controller/EateryController.java index 8d7971a..a708b4e 100644 --- a/gateway-service/src/main/java/com/drinkool/controller/EateryController.java +++ b/gateway-service/src/main/java/com/drinkool/controller/EateryController.java @@ -1,18 +1,29 @@ package com.drinkool.controller; import com.drinkool.InternalValue; +import com.drinkool.Url; +import com.drinkool.enums.SortBy; import com.drinkool.lib.dtos.GraphqlDto; +import com.drinkool.lib.dtos.SendReview; +import com.drinkool.services.ReviewService; import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient; import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder; +import io.smallrye.mutiny.Uni; import jakarta.inject.Inject; import jakarta.json.bind.Jsonb; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import java.util.UUID; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; @Path("/api/eatery") public class EateryController { @@ -23,6 +34,25 @@ public class EateryController { @Inject Jsonb jsonb; + @Inject + @RestClient + ReviewService reviewService; + + @GET + @Path(Url.Review + "/{eateryId}") + Uni getReviewsByEatery( + @PathParam("eateryId") UUID eateryId, + @QueryParam("sort") @DefaultValue("LATEST") SortBy sortBy + ) { + return reviewService.getReviewsByEatery(eateryId, sortBy); + } + + @POST + @Path(Url.Review) + Uni receiveReview(SendReview input) { + return reviewService.receiveReview(input); + } + @POST @Path("/graphql") public Response forward( diff --git a/gateway-service/src/main/java/com/drinkool/services/ReviewService.java b/gateway-service/src/main/java/com/drinkool/services/ReviewService.java new file mode 100644 index 0000000..42311bc --- /dev/null +++ b/gateway-service/src/main/java/com/drinkool/services/ReviewService.java @@ -0,0 +1,30 @@ +package com.drinkool.services; + +import com.drinkool.Url; +import com.drinkool.enums.SortBy; +import com.drinkool.lib.dtos.SendReview; + +import io.smallrye.mutiny.Uni; +import jakarta.ws.rs.DefaultValue; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Response; +import java.util.UUID; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +@RegisterRestClient(configKey = "eatery") +@Path(Url.Review) +public interface ReviewService { + @GET + @Path("/{eateryId}") + public Uni getReviewsByEatery( + @PathParam("eateryId") UUID eateryId, + @QueryParam("sort") @DefaultValue("LATEST") SortBy sortBy + ); + + @POST + public Uni receiveReview(SendReview input); +} diff --git a/gateway-service/src/main/resources/application.properties b/gateway-service/src/main/resources/application.properties index ec3257a..e919d67 100644 --- a/gateway-service/src/main/resources/application.properties +++ b/gateway-service/src/main/resources/application.properties @@ -14,6 +14,7 @@ quarkus.native.additional-build-args=--future-defaults=all # Rest Client quarkus.rest-client.account.url=http://account-service +quarkus.rest-client.eatery.url=http://eatery-service # GraphQL Client quarkus.smallrye-graphql-client.eatery.url=http://eatery-service/graphql \ No newline at end of file diff --git a/lib/pom.xml b/lib/pom.xml index bc83c1a..a1a0976 100644 --- a/lib/pom.xml +++ b/lib/pom.xml @@ -1,8 +1,8 @@ - + 4.0.0 @@ -18,6 +18,7 @@ 25 25 UTF-8 + 1.18.46 @@ -41,7 +42,7 @@ org.projectlombok lombok - 1.18.46 + ${org.projectlombok.version} provided @@ -67,4 +68,38 @@ quarkus-smallrye-graphql - + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + true + + + org.projectlombok + lombok + ${org.projectlombok.version} + + + + + + + io.smallrye + jandex-maven-plugin + 3.2.7 + + + make-index + + jandex + + + + + + + \ No newline at end of file diff --git a/lib/src/main/java/com/drinkool/Url.java b/lib/src/main/java/com/drinkool/Url.java index e547d3c..6e30f67 100644 --- a/lib/src/main/java/com/drinkool/Url.java +++ b/lib/src/main/java/com/drinkool/Url.java @@ -9,4 +9,5 @@ public class Url { public static final String SmsOtp = "/sms_otp"; public static final String Me = "/me"; public static final String Logout = "/logout"; + public static final String Review = "/review"; } diff --git a/lib/src/main/java/com/drinkool/enums/SortBy.java b/lib/src/main/java/com/drinkool/enums/SortBy.java new file mode 100644 index 0000000..1b34848 --- /dev/null +++ b/lib/src/main/java/com/drinkool/enums/SortBy.java @@ -0,0 +1,8 @@ +package com.drinkool.enums; + +public enum SortBy { + LATEST, + OLDEST, + HIGHEST, + LOWEST, +} diff --git a/lib/src/main/java/com/drinkool/interfaces/ReviewInterface.java b/lib/src/main/java/com/drinkool/interfaces/ReviewInterface.java new file mode 100644 index 0000000..8a006df --- /dev/null +++ b/lib/src/main/java/com/drinkool/interfaces/ReviewInterface.java @@ -0,0 +1,17 @@ +package com.drinkool.interfaces; + +import java.util.UUID; + +public interface ReviewInterface { + UUID getReviewerId(); + void setReviewerId(UUID x); + + UUID getEateryId(); + void setEateryId(UUID x); + + Integer getRating(); + void setRating(Integer x); + + String getComment(); + void setComment(String x); +} diff --git a/lib/src/main/java/com/drinkool/lib/dtos/SendReview.java b/lib/src/main/java/com/drinkool/lib/dtos/SendReview.java index 2793753..950aa58 100644 --- a/lib/src/main/java/com/drinkool/lib/dtos/SendReview.java +++ b/lib/src/main/java/com/drinkool/lib/dtos/SendReview.java @@ -1,11 +1,26 @@ package com.drinkool.lib.dtos; -import com.drinkool.lib.models.ReviewModel; +import com.drinkool.interfaces.ReviewInterface; +import io.quarkus.runtime.annotations.RegisterForReflection; import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; -public class SendReview { +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@RegisterForReflection +public class SendReview implements ReviewInterface { - public UUID reviewerId; - public UUID orderId; - public ReviewModel review; + private UUID reviewerId; + private UUID eateryId; + private Integer rating; + private String comment; + + public void setReviewerId(UUID x) { + return; + } } diff --git a/lib/src/main/java/com/drinkool/lib/entities/BaseEntity.java b/lib/src/main/java/com/drinkool/lib/entities/BaseEntity.java index 0a33368..a28ca88 100644 --- a/lib/src/main/java/com/drinkool/lib/entities/BaseEntity.java +++ b/lib/src/main/java/com/drinkool/lib/entities/BaseEntity.java @@ -4,7 +4,10 @@ import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; import java.util.UUID; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; @MappedSuperclass public class BaseEntity extends PanacheEntityBase { @@ -12,4 +15,10 @@ public class BaseEntity extends PanacheEntityBase { @jakarta.persistence.Id @GeneratedValue(strategy = GenerationType.UUID) public UUID id; + + @CreationTimestamp + public LocalDateTime createDate; + + @UpdateTimestamp + public LocalDateTime updateDate; } diff --git a/lib/src/main/java/com/drinkool/lib/models/ReviewModel.java b/lib/src/main/java/com/drinkool/lib/models/ReviewModel.java deleted file mode 100644 index 6412d6f..0000000 --- a/lib/src/main/java/com/drinkool/lib/models/ReviewModel.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.drinkool.lib.models; - -import com.drinkool.lib.entities.BaseEntity; -import jakarta.persistence.*; -import java.time.LocalDateTime; - -@MappedSuperclass -public class ReviewModel extends BaseEntity { - - @Column - public int rating; - - @Column - public String comment; - - public ReviewModel(int rating, String comment) { - this.rating = rating; - this.comment = comment; - } - - public ReviewModel() {} - - public ReviewModel(ReviewModel input) { - this(input.rating, input.comment); - } - - @org.hibernate.annotations.CreationTimestamp - @Column(name = "review_date", updatable = false) - public LocalDateTime reviewDate; -}