From 7ed7dee65863caba895bc6403e0e8a53a6d2ac15 Mon Sep 17 00:00:00 2001 From: Bartavius Date: Sat, 21 Feb 2026 13:37:22 -0500 Subject: [PATCH] added swaggoignore and new docs --- backend/docs/docs.go | 854 +++++++++++++++++++++++++++++++ backend/docs/swagger.json | 854 +++++++++++++++++++++++++++++++ backend/docs/swagger.yaml | 570 +++++++++++++++++++++ backend/internal/models/polls.go | 6 +- 4 files changed, 2281 insertions(+), 3 deletions(-) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index e96f531..9b42b3f 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -2177,6 +2177,613 @@ const docTemplate = `{ } } }, + "/api/v1/trips/{tripID}/vote-polls": { + "get": { + "description": "Retrieves all polls for a trip with cursor-based pagination", + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Get polls for a trip", + "operationId": "getPollsByTripID", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Max items per page (default 20, max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Opaque cursor returned in next_cursor", + "name": "cursor", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollCursorPageResult" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + }, + "post": { + "description": "Creates a new poll with initial options", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Create a poll", + "operationId": "createPoll", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "description": "Create poll request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreatePollRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + } + }, + "/api/v1/trips/{tripID}/vote-polls/{pollId}": { + "get": { + "description": "Retrieves a poll with vote counts and the caller's votes", + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Get poll results", + "operationId": "getPoll", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + }, + "delete": { + "description": "Deletes a poll and all associated options and votes, returns the deleted poll", + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Delete a poll", + "operationId": "deletePoll", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + }, + "patch": { + "description": "Updates poll metadata (question, deadline)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Update a poll", + "operationId": "updatePoll", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + }, + { + "description": "Update poll request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdatePollRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + } + }, + "/api/v1/trips/{tripID}/vote-polls/{pollId}/options": { + "post": { + "description": "Adds an option to a poll (only if no votes exist yet)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Add a poll option", + "operationId": "addPollOption", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + }, + { + "description": "Create option request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreatePollOptionRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.PollOption" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + } + }, + "/api/v1/trips/{tripID}/vote-polls/{pollId}/options/{optionId}": { + "delete": { + "description": "Removes an option from a poll and returns the deleted option", + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Delete a poll option", + "operationId": "deletePollOption", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Option ID", + "name": "optionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollOption" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + } + }, + "/api/v1/trips/{tripID}/vote-polls/{pollId}/vote": { + "post": { + "description": "Casts or replaces the user's vote(s) on a poll", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Cast a vote", + "operationId": "castVote", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + }, + { + "description": "Vote request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CastVoteRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + } + }, "/api/v1/trips/{tripID}/{entityType}/{entityID}/comments": { "get": { "description": "Retrieves paginated comments for a trip entity", @@ -2706,6 +3313,17 @@ const docTemplate = `{ } } }, + "models.CastVoteRequest": { + "type": "object", + "properties": { + "option_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "models.CategoryAPIResponse": { "type": "object", "properties": { @@ -2934,6 +3552,68 @@ const docTemplate = `{ } } }, + "models.CreatePollOptionRequest": { + "type": "object", + "required": [ + "name", + "option_type" + ], + "properties": { + "entity_id": { + "type": "string" + }, + "entity_type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "option_type": { + "enum": [ + "entity", + "custom" + ], + "allOf": [ + { + "$ref": "#/definitions/models.OptionType" + } + ] + } + } + }, + "models.CreatePollRequest": { + "type": "object", + "required": [ + "poll_type", + "question" + ], + "properties": { + "deadline": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/models.CreatePollOptionRequest" + } + }, + "poll_type": { + "enum": [ + "single", + "multi", + "rank" + ], + "allOf": [ + { + "$ref": "#/definitions/models.PollType" + } + ] + }, + "question": { + "type": "string" + } + } + }, "models.CreateTripInviteRequest": { "type": "object", "properties": { @@ -3226,6 +3906,17 @@ const docTemplate = `{ } } }, + "models.OptionType": { + "type": "string", + "enum": [ + "entity", + "custom" + ], + "x-enum-varnames": [ + "OptionTypeEntity", + "OptionTypeCustom" + ] + }, "models.PaginatedCommentsResponse": { "type": "object", "properties": { @@ -3441,6 +4132,158 @@ const docTemplate = `{ } } }, + "models.Poll": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "deadline": { + "type": "string" + }, + "id": { + "type": "string" + }, + "options": { + "description": "Relations", + "type": "array", + "items": { + "$ref": "#/definitions/models.PollOption" + } + }, + "poll_type": { + "$ref": "#/definitions/models.PollType" + }, + "question": { + "type": "string" + }, + "trip_id": { + "type": "string" + } + } + }, + "models.PollAPIResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "deadline": { + "type": "string" + }, + "id": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/models.PollOptionAPIResponse" + } + }, + "poll_type": { + "$ref": "#/definitions/models.PollType" + }, + "question": { + "type": "string" + }, + "trip_id": { + "type": "string" + } + } + }, + "models.PollCursorPageResult": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "limit": { + "type": "integer" + }, + "next_cursor": { + "type": "string" + } + } + }, + "models.PollOption": { + "type": "object", + "properties": { + "entity_id": { + "type": "string" + }, + "entity_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "option_type": { + "$ref": "#/definitions/models.OptionType" + }, + "poll": { + "description": "Relations", + "allOf": [ + { + "$ref": "#/definitions/models.Poll" + } + ] + }, + "poll_id": { + "type": "string" + } + } + }, + "models.PollOptionAPIResponse": { + "type": "object", + "properties": { + "entity_id": { + "type": "string" + }, + "entity_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "option_type": { + "$ref": "#/definitions/models.OptionType" + }, + "vote_count": { + "type": "integer" + }, + "voted": { + "type": "boolean" + } + } + }, + "models.PollType": { + "type": "string", + "enum": [ + "single", + "multi", + "rank" + ], + "x-enum-varnames": [ + "PollTypeSingle", + "PollTypeMulti", + "PollTypeRank" + ] + }, "models.S3HealthCheckResponse": { "type": "object", "properties": { @@ -3682,6 +4525,17 @@ const docTemplate = `{ } } }, + "models.UpdatePollRequest": { + "type": "object", + "properties": { + "deadline": { + "type": "string" + }, + "question": { + "type": "string" + } + } + }, "models.UpdateTripRequest": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 97cc736..2537c05 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -2171,6 +2171,613 @@ } } }, + "/api/v1/trips/{tripID}/vote-polls": { + "get": { + "description": "Retrieves all polls for a trip with cursor-based pagination", + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Get polls for a trip", + "operationId": "getPollsByTripID", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Max items per page (default 20, max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Opaque cursor returned in next_cursor", + "name": "cursor", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollCursorPageResult" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + }, + "post": { + "description": "Creates a new poll with initial options", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Create a poll", + "operationId": "createPoll", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "description": "Create poll request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreatePollRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + } + }, + "/api/v1/trips/{tripID}/vote-polls/{pollId}": { + "get": { + "description": "Retrieves a poll with vote counts and the caller's votes", + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Get poll results", + "operationId": "getPoll", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + }, + "delete": { + "description": "Deletes a poll and all associated options and votes, returns the deleted poll", + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Delete a poll", + "operationId": "deletePoll", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + }, + "patch": { + "description": "Updates poll metadata (question, deadline)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Update a poll", + "operationId": "updatePoll", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + }, + { + "description": "Update poll request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.UpdatePollRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + } + }, + "/api/v1/trips/{tripID}/vote-polls/{pollId}/options": { + "post": { + "description": "Adds an option to a poll (only if no votes exist yet)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Add a poll option", + "operationId": "addPollOption", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + }, + { + "description": "Create option request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CreatePollOptionRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.PollOption" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + } + }, + "/api/v1/trips/{tripID}/vote-polls/{pollId}/options/{optionId}": { + "delete": { + "description": "Removes an option from a poll and returns the deleted option", + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Delete a poll option", + "operationId": "deletePollOption", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Option ID", + "name": "optionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollOption" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "409": { + "description": "Conflict", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + } + }, + "/api/v1/trips/{tripID}/vote-polls/{pollId}/vote": { + "post": { + "description": "Casts or replaces the user's vote(s) on a poll", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "polls" + ], + "summary": "Cast a vote", + "operationId": "castVote", + "parameters": [ + { + "type": "string", + "description": "Trip ID", + "name": "tripID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Poll ID", + "name": "pollId", + "in": "path", + "required": true + }, + { + "description": "Vote request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CastVoteRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "422": { + "description": "Unprocessable Entity", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/errs.APIError" + } + } + } + } + }, "/api/v1/trips/{tripID}/{entityType}/{entityID}/comments": { "get": { "description": "Retrieves paginated comments for a trip entity", @@ -2700,6 +3307,17 @@ } } }, + "models.CastVoteRequest": { + "type": "object", + "properties": { + "option_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "models.CategoryAPIResponse": { "type": "object", "properties": { @@ -2928,6 +3546,68 @@ } } }, + "models.CreatePollOptionRequest": { + "type": "object", + "required": [ + "name", + "option_type" + ], + "properties": { + "entity_id": { + "type": "string" + }, + "entity_type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "option_type": { + "enum": [ + "entity", + "custom" + ], + "allOf": [ + { + "$ref": "#/definitions/models.OptionType" + } + ] + } + } + }, + "models.CreatePollRequest": { + "type": "object", + "required": [ + "poll_type", + "question" + ], + "properties": { + "deadline": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/models.CreatePollOptionRequest" + } + }, + "poll_type": { + "enum": [ + "single", + "multi", + "rank" + ], + "allOf": [ + { + "$ref": "#/definitions/models.PollType" + } + ] + }, + "question": { + "type": "string" + } + } + }, "models.CreateTripInviteRequest": { "type": "object", "properties": { @@ -3220,6 +3900,17 @@ } } }, + "models.OptionType": { + "type": "string", + "enum": [ + "entity", + "custom" + ], + "x-enum-varnames": [ + "OptionTypeEntity", + "OptionTypeCustom" + ] + }, "models.PaginatedCommentsResponse": { "type": "object", "properties": { @@ -3435,6 +4126,158 @@ } } }, + "models.Poll": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "deadline": { + "type": "string" + }, + "id": { + "type": "string" + }, + "options": { + "description": "Relations", + "type": "array", + "items": { + "$ref": "#/definitions/models.PollOption" + } + }, + "poll_type": { + "$ref": "#/definitions/models.PollType" + }, + "question": { + "type": "string" + }, + "trip_id": { + "type": "string" + } + } + }, + "models.PollAPIResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "deadline": { + "type": "string" + }, + "id": { + "type": "string" + }, + "options": { + "type": "array", + "items": { + "$ref": "#/definitions/models.PollOptionAPIResponse" + } + }, + "poll_type": { + "$ref": "#/definitions/models.PollType" + }, + "question": { + "type": "string" + }, + "trip_id": { + "type": "string" + } + } + }, + "models.PollCursorPageResult": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/models.PollAPIResponse" + } + }, + "limit": { + "type": "integer" + }, + "next_cursor": { + "type": "string" + } + } + }, + "models.PollOption": { + "type": "object", + "properties": { + "entity_id": { + "type": "string" + }, + "entity_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "option_type": { + "$ref": "#/definitions/models.OptionType" + }, + "poll": { + "description": "Relations", + "allOf": [ + { + "$ref": "#/definitions/models.Poll" + } + ] + }, + "poll_id": { + "type": "string" + } + } + }, + "models.PollOptionAPIResponse": { + "type": "object", + "properties": { + "entity_id": { + "type": "string" + }, + "entity_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "option_type": { + "$ref": "#/definitions/models.OptionType" + }, + "vote_count": { + "type": "integer" + }, + "voted": { + "type": "boolean" + } + } + }, + "models.PollType": { + "type": "string", + "enum": [ + "single", + "multi", + "rank" + ], + "x-enum-varnames": [ + "PollTypeSingle", + "PollTypeMulti", + "PollTypeRank" + ] + }, "models.S3HealthCheckResponse": { "type": "object", "properties": { @@ -3676,6 +4519,17 @@ } } }, + "models.UpdatePollRequest": { + "type": "object", + "properties": { + "deadline": { + "type": "string" + }, + "question": { + "type": "string" + } + } + }, "models.UpdateTripRequest": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 9518e10..a0dda5a 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -110,6 +110,13 @@ definitions: southwest: $ref: '#/definitions/models.LatLng' type: object + models.CastVoteRequest: + properties: + option_ids: + items: + type: string + type: array + type: object models.CategoryAPIResponse: properties: created_at: @@ -261,6 +268,45 @@ definitions: - trip_id - user_id type: object + models.CreatePollOptionRequest: + properties: + entity_id: + type: string + entity_type: + type: string + name: + type: string + option_type: + allOf: + - $ref: '#/definitions/models.OptionType' + enum: + - entity + - custom + required: + - name + - option_type + type: object + models.CreatePollRequest: + properties: + deadline: + type: string + options: + items: + $ref: '#/definitions/models.CreatePollOptionRequest' + type: array + poll_type: + allOf: + - $ref: '#/definitions/models.PollType' + enum: + - single + - multi + - rank + question: + type: string + required: + - poll_type + - question + type: object models.CreateTripInviteRequest: properties: expires_at: @@ -458,6 +504,14 @@ definitions: type: string type: array type: object + models.OptionType: + enum: + - entity + - custom + type: string + x-enum-varnames: + - OptionTypeEntity + - OptionTypeCustom models.PaginatedCommentsResponse: properties: items: @@ -608,6 +662,106 @@ definitions: status: type: string type: object + models.Poll: + properties: + created_at: + type: string + created_by: + type: string + deadline: + type: string + id: + type: string + options: + description: Relations + items: + $ref: '#/definitions/models.PollOption' + type: array + poll_type: + $ref: '#/definitions/models.PollType' + question: + type: string + trip_id: + type: string + type: object + models.PollAPIResponse: + properties: + created_at: + type: string + created_by: + type: string + deadline: + type: string + id: + type: string + options: + items: + $ref: '#/definitions/models.PollOptionAPIResponse' + type: array + poll_type: + $ref: '#/definitions/models.PollType' + question: + type: string + trip_id: + type: string + type: object + models.PollCursorPageResult: + properties: + items: + items: + $ref: '#/definitions/models.PollAPIResponse' + type: array + limit: + type: integer + next_cursor: + type: string + type: object + models.PollOption: + properties: + entity_id: + type: string + entity_type: + type: string + id: + type: string + name: + type: string + option_type: + $ref: '#/definitions/models.OptionType' + poll: + allOf: + - $ref: '#/definitions/models.Poll' + description: Relations + poll_id: + type: string + type: object + models.PollOptionAPIResponse: + properties: + entity_id: + type: string + entity_type: + type: string + id: + type: string + name: + type: string + option_type: + $ref: '#/definitions/models.OptionType' + vote_count: + type: integer + voted: + type: boolean + type: object + models.PollType: + enum: + - single + - multi + - rank + type: string + x-enum-varnames: + - PollTypeSingle + - PollTypeMulti + - PollTypeRank models.S3HealthCheckResponse: properties: bucketName: @@ -773,6 +927,13 @@ definitions: is_admin: type: boolean type: object + models.UpdatePollRequest: + properties: + deadline: + type: string + question: + type: string + type: object models.UpdateTripRequest: properties: budget_max: @@ -2377,6 +2538,415 @@ paths: summary: Promote member to admin tags: - memberships + /api/v1/trips/{tripID}/vote-polls: + get: + description: Retrieves all polls for a trip with cursor-based pagination + operationId: getPollsByTripID + parameters: + - description: Trip ID + in: path + name: tripID + required: true + type: string + - description: Max items per page (default 20, max 100) + in: query + name: limit + type: integer + - description: Opaque cursor returned in next_cursor + in: query + name: cursor + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.PollCursorPageResult' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errs.APIError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/errs.APIError' + "403": + description: Forbidden + schema: + $ref: '#/definitions/errs.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/errs.APIError' + summary: Get polls for a trip + tags: + - polls + post: + consumes: + - application/json + description: Creates a new poll with initial options + operationId: createPoll + parameters: + - description: Trip ID + in: path + name: tripID + required: true + type: string + - description: Create poll request + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.CreatePollRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.PollAPIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errs.APIError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/errs.APIError' + "403": + description: Forbidden + schema: + $ref: '#/definitions/errs.APIError' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/errs.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/errs.APIError' + summary: Create a poll + tags: + - polls + /api/v1/trips/{tripID}/vote-polls/{pollId}: + delete: + description: Deletes a poll and all associated options and votes, returns the + deleted poll + operationId: deletePoll + parameters: + - description: Trip ID + in: path + name: tripID + required: true + type: string + - description: Poll ID + in: path + name: pollId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.PollAPIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errs.APIError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/errs.APIError' + "403": + description: Forbidden + schema: + $ref: '#/definitions/errs.APIError' + "404": + description: Not Found + schema: + $ref: '#/definitions/errs.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/errs.APIError' + summary: Delete a poll + tags: + - polls + get: + description: Retrieves a poll with vote counts and the caller's votes + operationId: getPoll + parameters: + - description: Trip ID + in: path + name: tripID + required: true + type: string + - description: Poll ID + in: path + name: pollId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.PollAPIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errs.APIError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/errs.APIError' + "403": + description: Forbidden + schema: + $ref: '#/definitions/errs.APIError' + "404": + description: Not Found + schema: + $ref: '#/definitions/errs.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/errs.APIError' + summary: Get poll results + tags: + - polls + patch: + consumes: + - application/json + description: Updates poll metadata (question, deadline) + operationId: updatePoll + parameters: + - description: Trip ID + in: path + name: tripID + required: true + type: string + - description: Poll ID + in: path + name: pollId + required: true + type: string + - description: Update poll request + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.UpdatePollRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.PollAPIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errs.APIError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/errs.APIError' + "403": + description: Forbidden + schema: + $ref: '#/definitions/errs.APIError' + "404": + description: Not Found + schema: + $ref: '#/definitions/errs.APIError' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/errs.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/errs.APIError' + summary: Update a poll + tags: + - polls + /api/v1/trips/{tripID}/vote-polls/{pollId}/options: + post: + consumes: + - application/json + description: Adds an option to a poll (only if no votes exist yet) + operationId: addPollOption + parameters: + - description: Trip ID + in: path + name: tripID + required: true + type: string + - description: Poll ID + in: path + name: pollId + required: true + type: string + - description: Create option request + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.CreatePollOptionRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/models.PollOption' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errs.APIError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/errs.APIError' + "403": + description: Forbidden + schema: + $ref: '#/definitions/errs.APIError' + "404": + description: Not Found + schema: + $ref: '#/definitions/errs.APIError' + "409": + description: Conflict + schema: + $ref: '#/definitions/errs.APIError' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/errs.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/errs.APIError' + summary: Add a poll option + tags: + - polls + /api/v1/trips/{tripID}/vote-polls/{pollId}/options/{optionId}: + delete: + description: Removes an option from a poll and returns the deleted option + operationId: deletePollOption + parameters: + - description: Trip ID + in: path + name: tripID + required: true + type: string + - description: Poll ID + in: path + name: pollId + required: true + type: string + - description: Option ID + in: path + name: optionId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.PollOption' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errs.APIError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/errs.APIError' + "403": + description: Forbidden + schema: + $ref: '#/definitions/errs.APIError' + "404": + description: Not Found + schema: + $ref: '#/definitions/errs.APIError' + "409": + description: Conflict + schema: + $ref: '#/definitions/errs.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/errs.APIError' + summary: Delete a poll option + tags: + - polls + /api/v1/trips/{tripID}/vote-polls/{pollId}/vote: + post: + consumes: + - application/json + description: Casts or replaces the user's vote(s) on a poll + operationId: castVote + parameters: + - description: Trip ID + in: path + name: tripID + required: true + type: string + - description: Poll ID + in: path + name: pollId + required: true + type: string + - description: Vote request + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.CastVoteRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.PollAPIResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errs.APIError' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/errs.APIError' + "403": + description: Forbidden + schema: + $ref: '#/definitions/errs.APIError' + "404": + description: Not Found + schema: + $ref: '#/definitions/errs.APIError' + "422": + description: Unprocessable Entity + schema: + $ref: '#/definitions/errs.APIError' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/errs.APIError' + summary: Cast a vote + tags: + - polls /api/v1/users: post: consumes: diff --git a/backend/internal/models/polls.go b/backend/internal/models/polls.go index 00b1aeb..108a716 100644 --- a/backend/internal/models/polls.go +++ b/backend/internal/models/polls.go @@ -24,7 +24,7 @@ const ( // Poll represents a voting poll attached to a trip. type Poll struct { - bun.BaseModel `bun:"table:polls,alias:p"` + bun.BaseModel `bun:"table:polls,alias:p" swaggerignore:"true"` ID uuid.UUID `bun:"id,pk,type:uuid" json:"id"` TripID uuid.UUID `bun:"trip_id,type:uuid,notnull" json:"trip_id"` @@ -40,7 +40,7 @@ type Poll struct { // PollOption represents a single selectable option within a poll. type PollOption struct { - bun.BaseModel `bun:"table:poll_options,alias:po"` + bun.BaseModel `bun:"table:poll_options,alias:po" swaggerignore:"true"` ID uuid.UUID `bun:"id,pk,type:uuid" json:"id"` PollID uuid.UUID `bun:"poll_id,type:uuid,notnull" json:"poll_id"` @@ -57,7 +57,7 @@ type PollOption struct { // only vote once per option. For single-choice polls, exactly one row exists // per user; for multi-choice, one row per selected option. type PollVote struct { - bun.BaseModel `bun:"table:poll_votes,alias:pv"` + bun.BaseModel `bun:"table:poll_votes,alias:pv" swaggerignore:"true"` PollID uuid.UUID `bun:"poll_id,pk,type:uuid,notnull" json:"poll_id"` OptionID uuid.UUID `bun:"option_id,pk,type:uuid,notnull" json:"option_id"`