Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/astrxnomo/tourify/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The reviews system in Tourify is polymorphic — the same reviews table stores ratings for both places and events using reviewable_type and reviewable_id columns. Each user can leave exactly one review per item, and re-submitting a review updates the existing one rather than creating a duplicate, thanks to updateOrCreate.
All review endpoints require authentication. Include the Authorization: Bearer {your-token} header on every request.

How upsert works

The backend uses Eloquent’s updateOrCreate to find or create a review keyed on (user_id, reviewable_id, reviewable_type). If the user has already reviewed the item, the rating and comment fields are updated in place.
ReviewController.php (excerpt)
$review = Review::updateOrCreate(
    [
        'user_id'         => $request->user()->id,
        'reviewable_id'   => $reviewable->id,
        'reviewable_type' => $type,
    ],
    [
        'rating'  => $data['rating'],
        'comment' => $data['comment'] ?? null,
    ]
);
This means calling the endpoint twice with different ratings will not create two records — the second call updates the first.

Endpoints

POST /api/places//reviews

Submit or update a review for a place. The response always returns HTTP 201 whether the review was created or updated. Path parameter
place
integer
required
The ID of the place to review.
Body parameters
rating
integer
required
An integer star rating between 1 and 5 (inclusive).
comment
string
An optional written comment. Maximum 1 000 characters. Send null or omit to leave no comment.
curl -s -X POST http://localhost:8000/api/places/10/reviews \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer 15|xYzAbCdEfGhIjKlMnOpQrStUvWx0987654321" \
  -d '{
    "rating": 5,
    "comment": "Una obra de arte impresionante. Visita obligada en Barcelona."
  }'

POST /api/events//reviews

Submit or update a review for an event. Identical validation rules and response shape as the place review endpoint. Path parameter
event
integer
required
The ID of the event to review.
Body parameters
rating
integer
required
An integer star rating between 1 and 5 (inclusive).
comment
string
An optional written comment. Maximum 1 000 characters.
curl -s -X POST http://localhost:8000/api/events/7/reviews \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Bearer 15|xYzAbCdEfGhIjKlMnOpQrStUvWx0987654321" \
  -d '{
    "rating": 4,
    "comment": "Una velada memorable. El guía fue fantástico."
  }'

Average ratings

Review averages are computed on the server and surfaced in the list and detail responses for both places and events:
EndpointRating field
GET /api/placesaverage_rating (rounded to 1 decimal, results sorted by this value descending)
GET /api/places/{place}average_rating (arithmetic mean of all reviews.rating)
GET /api/events/{event}Not included as a top-level field, but the reviews array contains individual ratings
Items with no reviews have average_rating: 0.0.

Frontend integration

The useReviewForm hook manages the star picker and comment input. It dispatches to the correct endpoint based on the type prop ("place" or "event"):
hooks/useReviewForm.js
import { useCallback, useState } from "react";
import { Alert } from "react-native";
import { post } from "../services/post";

export function useReviewForm({ type, id, onSuccess }) {
  const [rating, setRating] = useState(0);
  const [comment, setComment] = useState("");
  const [submitting, setSubmitting] = useState(false);

  const submit = useCallback(async () => {
    if (rating < 1) {
      Alert.alert("Califica primero", "Selecciona de 1 a 5 estrellas.");
      return;
    }
    setSubmitting(true);
    try {
      // Dynamically builds the endpoint for place or event
      const endpoint =
        type === "place" ? `/places/${id}/reviews` : `/events/${id}/reviews`;
      await post(endpoint, { rating, comment: comment.trim() || null });
      setRating(0);
      setComment("");
      onSuccess?.(); // e.g. close the modal and refresh the place detail
    } catch (err) {
      Alert.alert("Error", err.message || "No se pudo enviar la reseña.");
    } finally {
      setSubmitting(false);
    }
  }, [type, id, rating, comment, onSuccess]);

  return { rating, setRating, comment, setComment, submitting, submit };
}
Usage in a screen:
PlaceDetailScreen.jsx (excerpt)
const { rating, setRating, comment, setComment, submitting, submit } =
  useReviewForm({
    type: "place",
    id: place.id,
    onSuccess: () => {
      setReviewModalVisible(false);
      refreshPlace(); // reload the place to show the updated average_rating
    },
  });

Validation reference

FieldRule
ratingRequired · Integer · Min: 1 · Max: 5
commentOptional · String · Max: 1 000 characters

Build docs developers (and LLMs) love