using FluentValidation; using Microsoft.AspNetCore.Mvc; using Meezi.API.Models.Public; using Meezi.API.Services; using Meezi.Core.Authorization; using Meezi.Core.Enums; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Services.Platform; using Meezi.Shared; namespace Meezi.API.Controllers; [Route("api/cafes/{cafeId}/reviews")] public class CafeReviewsController : CafeApiControllerBase { private readonly IReviewService _reviews; private readonly IValidator _replyValidator; private readonly IPlatformCatalogService _catalog; public CafeReviewsController( IReviewService reviews, IValidator replyValidator, IPlatformCatalogService catalog) { _reviews = reviews; _replyValidator = replyValidator; _catalog = catalog; } [HttpGet] public async Task List( string cafeId, ITenantContext tenant, CancellationToken ct, [FromQuery] int page = 1, [FromQuery] int pageSize = 20) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; var data = await _reviews.GetReviewsAsync(cafeId, page, pageSize, publicOnly: false, ct); return Ok(new ApiResponse>(true, data)); } [HttpPatch("{reviewId}/reply")] public async Task Reply( string cafeId, string reviewId, [FromBody] ReplyCafeReviewRequest request, ITenantContext tenant, CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.ManageReviews) is { } permDenied) return permDenied; // Replying to reviews is a paid feature (Starter+). var tier = tenant.PlanTier ?? PlanTier.Free; if (!await _catalog.IsFeatureEnabledForCafeAsync(cafeId, tier, "review_reply", ct)) return StatusCode(403, new ApiResponse(false, null, new ApiError("PLAN_FEATURE_DISABLED", "Replying to reviews is not included in your plan. Please upgrade."))); var validation = await _replyValidator.ValidateAsync(request, ct); if (!validation.IsValid) { var first = validation.Errors.First(); return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", first.ErrorMessage, first.PropertyName))); } var data = await _reviews.ReplyReviewAsync(cafeId, reviewId, request.Reply, ct); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); } [HttpPatch("{reviewId}/visibility")] public async Task SetVisibility( string cafeId, string reviewId, [FromBody] HideCafeReviewRequest request, ITenantContext tenant, CancellationToken ct = default) { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; if (EnsurePermission(tenant, Permission.ManageReviews) is { } permDenied) return permDenied; var data = await _reviews.SetHiddenAsync(cafeId, reviewId, request.IsHidden, ct); if (data is null) return NotFoundError(); return Ok(new ApiResponse(true, data)); } }