AdminReviewController.java

package com.ecommerce.catalog.interfaces.rest;

import com.ecommerce.catalog.application.model.ReviewModels.ModerationResultView;
import com.ecommerce.catalog.application.model.ReviewModels.ProductReviewView;
import com.ecommerce.catalog.application.model.ReviewModels.ReviewReportView;
import com.ecommerce.catalog.application.service.ProductReviewService;
import com.ecommerce.platform.common.api.ApiResponse;
import com.ecommerce.platform.common.api.PageResponse;
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.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;

@Validated
@RestController
@RequestMapping("/api/v1/catalog/admin/reviews")
public class AdminReviewController {

    private final ProductReviewService service;

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

    @GetMapping("/reports")
    public ApiResponse<PageResponse<ReviewReportView>> reports(
            @RequestParam(required = false)
            @Pattern(regexp = "OPEN|RESOLVED") String status,
            @RequestParam(defaultValue = "1") @Min(1) long page,
            @RequestParam(defaultValue = "20") @Min(1) @Max(100) long size) {
        return ApiResponse.success(service.listReports(status, page, size));
    }

    @PostMapping("/{reviewId}/reply")
    public ApiResponse<ProductReviewView> reply(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Positive long reviewId,
            @RequestHeader("Idempotency-Key")
            @NotBlank @Size(max = 64) String commandId,
            @Valid @RequestBody ReplyRequest request) {
        return ApiResponse.success(service.reply(
                userId(jwt),
                reviewId,
                commandId,
                request.content()));
    }

    @PostMapping("/reports/{reportId}/resolve")
    public ApiResponse<ModerationResultView> resolve(
            @AuthenticationPrincipal Jwt jwt,
            @PathVariable @Positive long reportId,
            @Valid @RequestBody ResolveReportRequest request) {
        return ApiResponse.success(service.resolveReport(
                userId(jwt),
                reportId,
                request.commandId(),
                request.resolution(),
                request.reason()));
    }

    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 ReplyRequest(
            @NotBlank @Size(max = 1000) String content) {
    }

    public record ResolveReportRequest(
            @NotBlank @Size(max = 64) String commandId,
            @NotBlank @Pattern(regexp = "UPHELD|REJECTED") String resolution,
            @NotBlank @Size(min = 8, max = 500) String reason) {
    }
}