CustomerReviewController.java

package com.ecommerce.catalog.interfaces.rest;

import com.ecommerce.catalog.application.model.ReviewModels.CreateReviewCommand;
import com.ecommerce.catalog.application.model.ReviewModels.ProductReviewView;
import com.ecommerce.catalog.application.model.ReviewModels.ReviewEligibilityView;
import com.ecommerce.catalog.application.model.ReviewModels.ReviewReportReceipt;
import com.ecommerce.catalog.application.service.ProductReviewService;
import com.ecommerce.platform.common.api.ApiResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Validated
@RestController
@RequestMapping("/api/v1/catalog")
public class CustomerReviewController {

    private final ProductReviewService service;

    public CustomerReviewController(ProductReviewService service) {
        this.service = service;
    }

    @GetMapping("/review-eligibilities")
    public ApiResponse<List<ReviewEligibilityView>> eligibilities(
            @AuthenticationPrincipal Jwt jwt,
            @RequestParam(required = false) @Size(max = 64) String orderNo) {
        return ApiResponse.success(service.listEligibilities(userId(jwt), orderNo));
    }

    @PostMapping("/reviews")
    public ApiResponse<ProductReviewView> create(
            @AuthenticationPrincipal Jwt jwt,
            @RequestHeader("Idempotency-Key")
            @NotBlank @Size(max = 64) String idempotencyKey,
            @Valid @RequestBody CreateReviewRequest request) {
        return ApiResponse.success(service.createReview(new CreateReviewCommand(
                userId(jwt),
                request.eligibilityId(),
                request.rating(),
                request.content(),
                request.anonymous(),
                idempotencyKey)));
    }

    @PostMapping("/reviews/{reviewId}/likes")
    public ApiResponse<ProductReviewView> like(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Positive long reviewId) {
        return ApiResponse.success(service.like(userId(jwt), reviewId));
    }

    @DeleteMapping("/reviews/{reviewId}/likes")
    public ApiResponse<ProductReviewView> unlike(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Positive long reviewId) {
        return ApiResponse.success(service.unlike(userId(jwt), reviewId));
    }

    @PostMapping("/reviews/{reviewId}/reports")
    public ApiResponse<ReviewReportReceipt> report(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Positive long reviewId,
            @Valid @RequestBody ReportReviewRequest request) {
        return ApiResponse.success(service.report(
                userId(jwt),
                reviewId,
                request.reasonCode(),
                request.detail()));
    }

    private long userId(Jwt jwt) {
        try {
            long value = Long.parseLong(jwt.getSubject());
            if (value <= 0) {
                throw new IllegalArgumentException("JWT subject must be positive");
            }
            return value;
        } catch (RuntimeException exception) {
            throw new IllegalArgumentException("JWT subject is invalid", exception);
        }
    }

    public record CreateReviewRequest(
            @Positive Long eligibilityId,
            @Min(1) @Max(5) int rating,
            @NotBlank @Size(max = 2000) String content,
            boolean anonymous) {
    }

    public record ReportReviewRequest(
            @NotBlank
            @Pattern(regexp = "SPAM|ABUSE|FALSE_INFORMATION|OTHER")
            String reasonCode,
            @Size(max = 500) String detail) {
    }
}