Add swagger spec for visits api

This commit is contained in:
Eugene Burmakin
2025-08-21 20:53:23 +02:00
parent 550d20c555
commit a4605f9b3e
4 changed files with 811 additions and 14 deletions

View File

@@ -7,7 +7,6 @@ module Visits
def initialize(user, params)
@user = user
@params = params.respond_to?(:with_indifferent_access) ? params.with_indifferent_access : params
@errors = []
@visit = nil
end
@@ -18,12 +17,8 @@ module Visits
create_visit(place)
end
rescue ActiveRecord::RecordInvalid => e
@errors = e.record.errors.full_messages
false
rescue StandardError => e
ExceptionReporter.call(e, 'Failed to create visit')
@errors = [e.message]
false
end
@@ -60,12 +55,8 @@ module Visits
)
place
rescue ActiveRecord::RecordInvalid => e
@errors = e.record.errors.full_messages
nil
rescue StandardError => e
ExceptionReporter.call(e, 'Failed to create place')
@errors = [e.message]
nil
end

View File

@@ -46,11 +46,6 @@ RSpec.describe Visits::Create do
expect(place.longitude).to eq(13.405)
expect(place.source).to eq('manual')
end
it 'has no errors' do
service.call
expect(service.errors).to be_empty
end
end
context 'when reusing existing place' do

View File

@@ -0,0 +1,393 @@
# frozen_string_literal: true
require 'swagger_helper'
describe 'Visits API', type: :request do
let(:user) { create(:user) }
let(:api_key) { user.api_key }
let(:place) { create(:place) }
let(:test_visit) { create(:visit, user: user, place: place) }
path '/api/v1/visits' do
get 'List visits' do
tags 'Visits'
produces 'application/json'
parameter name: :Authorization, in: :header, type: :string, required: true, description: 'Bearer token'
parameter name: :start_at, in: :query, type: :string, required: false, description: 'Start date (ISO 8601)'
parameter name: :end_at, in: :query, type: :string, required: false, description: 'End date (ISO 8601)'
parameter name: :selection, in: :query, type: :string, required: false, description: 'Set to "true" for area-based search'
parameter name: :sw_lat, in: :query, type: :number, required: false, description: 'Southwest latitude for area search'
parameter name: :sw_lng, in: :query, type: :number, required: false, description: 'Southwest longitude for area search'
parameter name: :ne_lat, in: :query, type: :number, required: false, description: 'Northeast latitude for area search'
parameter name: :ne_lng, in: :query, type: :number, required: false, description: 'Northeast longitude for area search'
response '200', 'visits found' do
let(:Authorization) { "Bearer #{api_key}" }
let(:start_at) { 1.week.ago.iso8601 }
let(:end_at) { Time.current.iso8601 }
schema type: :array,
items: {
type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
status: { type: :string, enum: %w[suggested confirmed declined] },
started_at: { type: :string, format: :datetime },
ended_at: { type: :string, format: :datetime },
duration: { type: :integer, description: 'Duration in minutes' },
place: {
type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
latitude: { type: :number },
longitude: { type: :number },
city: { type: :string },
country: { type: :string }
}
}
},
required: %w[id name status started_at ended_at duration]
}
run_test!
end
response '401', 'unauthorized' do
let(:Authorization) { 'Bearer invalid-token' }
run_test!
end
end
post 'Create visit' do
tags 'Visits'
consumes 'application/json'
produces 'application/json'
parameter name: :Authorization, in: :header, type: :string, required: true, description: 'Bearer token'
parameter name: :visit, in: :body, schema: {
type: :object,
properties: {
visit: {
type: :object,
properties: {
name: { type: :string },
latitude: { type: :number },
longitude: { type: :number },
started_at: { type: :string, format: :datetime },
ended_at: { type: :string, format: :datetime }
},
required: %w[name latitude longitude started_at ended_at]
}
}
}
response '200', 'visit created' do
let(:Authorization) { "Bearer #{api_key}" }
let(:visit) do
{
visit: {
name: 'Test Visit',
latitude: 52.52,
longitude: 13.405,
started_at: '2023-12-01T10:00:00Z',
ended_at: '2023-12-01T12:00:00Z'
}
}
end
schema type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
status: { type: :string },
started_at: { type: :string, format: :datetime },
ended_at: { type: :string, format: :datetime },
duration: { type: :integer },
place: {
type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
latitude: { type: :number },
longitude: { type: :number }
}
}
}
run_test!
end
response '422', 'invalid request' do
let(:Authorization) { "Bearer #{api_key}" }
let(:visit) do
{
visit: {
name: '',
latitude: 52.52,
longitude: 13.405,
started_at: '2023-12-01T10:00:00Z',
ended_at: '2023-12-01T12:00:00Z'
}
}
end
run_test!
end
response '401', 'unauthorized' do
let(:Authorization) { 'Bearer invalid-token' }
let(:visit) do
{
visit: {
name: 'Test Visit',
latitude: 52.52,
longitude: 13.405,
started_at: '2023-12-01T10:00:00Z',
ended_at: '2023-12-01T12:00:00Z'
}
}
end
run_test!
end
end
end
path '/api/v1/visits/{id}' do
patch 'Update visit' do
tags 'Visits'
consumes 'application/json'
produces 'application/json'
parameter name: :id, in: :path, type: :integer, required: true, description: 'Visit ID'
parameter name: :Authorization, in: :header, type: :string, required: true, description: 'Bearer token'
parameter name: :visit, in: :body, schema: {
type: :object,
properties: {
visit: {
type: :object,
properties: {
name: { type: :string },
place_id: { type: :integer },
status: { type: :string, enum: %w[suggested confirmed declined] }
}
}
}
}
response '200', 'visit updated' do
let(:Authorization) { "Bearer #{api_key}" }
let(:id) { test_visit.id }
let(:visit) { { visit: { name: 'Updated Visit' } } }
schema type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
status: { type: :string },
started_at: { type: :string, format: :datetime },
ended_at: { type: :string, format: :datetime },
duration: { type: :integer },
place: { type: :object }
}
run_test!
end
response '404', 'visit not found' do
let(:Authorization) { "Bearer #{api_key}" }
let(:id) { 999999 }
let(:visit) { { visit: { name: 'Updated Visit' } } }
run_test!
end
response '401', 'unauthorized' do
let(:Authorization) { 'Bearer invalid-token' }
let(:id) { test_visit.id }
let(:visit) { { visit: { name: 'Updated Visit' } } }
run_test!
end
end
delete 'Delete visit' do
tags 'Visits'
parameter name: :id, in: :path, type: :integer, required: true, description: 'Visit ID'
parameter name: :Authorization, in: :header, type: :string, required: true, description: 'Bearer token'
response '204', 'visit deleted' do
let(:Authorization) { "Bearer #{api_key}" }
let(:id) { test_visit.id }
run_test!
end
response '404', 'visit not found' do
let(:Authorization) { "Bearer #{api_key}" }
let(:id) { 999999 }
run_test!
end
response '401', 'unauthorized' do
let(:Authorization) { 'Bearer invalid-token' }
let(:id) { test_visit.id }
run_test!
end
end
end
path '/api/v1/visits/{id}/possible_places' do
get 'Get possible places for visit' do
tags 'Visits'
produces 'application/json'
parameter name: :id, in: :path, type: :integer, required: true, description: 'Visit ID'
parameter name: :Authorization, in: :header, type: :string, required: true, description: 'Bearer token'
response '200', 'possible places found' do
let(:Authorization) { "Bearer #{api_key}" }
let(:id) { test_visit.id }
schema type: :array,
items: {
type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
latitude: { type: :number },
longitude: { type: :number },
city: { type: :string },
country: { type: :string }
}
}
run_test!
end
response '404', 'visit not found' do
let(:Authorization) { "Bearer #{api_key}" }
let(:id) { 999999 }
run_test!
end
response '401', 'unauthorized' do
let(:Authorization) { 'Bearer invalid-token' }
let(:id) { test_visit.id }
run_test!
end
end
end
path '/api/v1/visits/merge' do
post 'Merge visits' do
tags 'Visits'
consumes 'application/json'
produces 'application/json'
parameter name: :Authorization, in: :header, type: :string, required: true, description: 'Bearer token'
parameter name: :merge_params, in: :body, schema: {
type: :object,
properties: {
visit_ids: {
type: :array,
items: { type: :integer },
minItems: 2,
description: 'Array of visit IDs to merge (minimum 2)'
}
},
required: %w[visit_ids]
}
response '200', 'visits merged' do
let(:Authorization) { "Bearer #{api_key}" }
let(:visit1) { create(:visit, user: user) }
let(:visit2) { create(:visit, user: user) }
let(:merge_params) { { visit_ids: [visit1.id, visit2.id] } }
schema type: :object,
properties: {
id: { type: :integer },
name: { type: :string },
status: { type: :string },
started_at: { type: :string, format: :datetime },
ended_at: { type: :string, format: :datetime },
duration: { type: :integer },
place: { type: :object }
}
run_test!
end
response '422', 'invalid request' do
let(:Authorization) { "Bearer #{api_key}" }
let(:merge_params) { { visit_ids: [test_visit.id] } }
run_test!
end
response '401', 'unauthorized' do
let(:Authorization) { 'Bearer invalid-token' }
let(:merge_params) { { visit_ids: [test_visit.id] } }
run_test!
end
end
end
path '/api/v1/visits/bulk_update' do
post 'Bulk update visits' do
tags 'Visits'
consumes 'application/json'
produces 'application/json'
parameter name: :Authorization, in: :header, type: :string, required: true, description: 'Bearer token'
parameter name: :bulk_params, in: :body, schema: {
type: :object,
properties: {
visit_ids: {
type: :array,
items: { type: :integer },
description: 'Array of visit IDs to update'
},
status: {
type: :string,
enum: %w[suggested confirmed declined],
description: 'New status for the visits'
}
},
required: %w[visit_ids status]
}
response '200', 'visits updated' do
let(:Authorization) { "Bearer #{api_key}" }
let(:visit1) { create(:visit, user: user, status: 'suggested') }
let(:visit2) { create(:visit, user: user, status: 'suggested') }
let(:bulk_params) { { visit_ids: [visit1.id, visit2.id], status: 'confirmed' } }
schema type: :object,
properties: {
message: { type: :string },
updated_count: { type: :integer }
}
run_test!
end
response '422', 'invalid request' do
let(:Authorization) { "Bearer #{api_key}" }
let(:bulk_params) { { visit_ids: [test_visit.id], status: 'invalid_status' } }
run_test!
end
response '401', 'unauthorized' do
let(:Authorization) { 'Bearer invalid-token' }
let(:bulk_params) { { visit_ids: [test_visit.id], status: 'confirmed' } }
run_test!
end
end
end
end

View File

@@ -1275,6 +1275,424 @@ paths:
responses:
'200':
description: user found
"/api/v1/visits":
get:
summary: List visits
tags:
- Visits
parameters:
- name: Authorization
in: header
required: true
description: Bearer token
schema:
type: string
- name: start_at
in: query
required: false
description: Start date (ISO 8601)
schema:
type: string
- name: end_at
in: query
required: false
description: End date (ISO 8601)
schema:
type: string
- name: selection
in: query
required: false
description: Set to "true" for area-based search
schema:
type: string
- name: sw_lat
in: query
required: false
description: Southwest latitude for area search
schema:
type: number
- name: sw_lng
in: query
required: false
description: Southwest longitude for area search
schema:
type: number
- name: ne_lat
in: query
required: false
description: Northeast latitude for area search
schema:
type: number
- name: ne_lng
in: query
required: false
description: Northeast longitude for area search
schema:
type: number
responses:
'200':
description: visits found
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
status:
type: string
enum:
- suggested
- confirmed
- declined
started_at:
type: string
format: datetime
ended_at:
type: string
format: datetime
duration:
type: integer
description: Duration in minutes
place:
type: object
properties:
id:
type: integer
name:
type: string
latitude:
type: number
longitude:
type: number
city:
type: string
country:
type: string
required:
- id
- name
- status
- started_at
- ended_at
- duration
'401':
description: unauthorized
post:
summary: Create visit
tags:
- Visits
parameters:
- name: Authorization
in: header
required: true
description: Bearer token
schema:
type: string
responses:
'200':
description: visit created
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
status:
type: string
started_at:
type: string
format: datetime
ended_at:
type: string
format: datetime
duration:
type: integer
place:
type: object
properties:
id:
type: integer
name:
type: string
latitude:
type: number
longitude:
type: number
'422':
description: invalid request
'401':
description: unauthorized
requestBody:
content:
application/json:
schema:
type: object
properties:
visit:
type: object
properties:
name:
type: string
latitude:
type: number
longitude:
type: number
started_at:
type: string
format: datetime
ended_at:
type: string
format: datetime
required:
- name
- latitude
- longitude
- started_at
- ended_at
"/api/v1/visits/{id}":
patch:
summary: Update visit
tags:
- Visits
parameters:
- name: id
in: path
required: true
description: Visit ID
schema:
type: integer
- name: Authorization
in: header
required: true
description: Bearer token
schema:
type: string
responses:
'200':
description: visit updated
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
status:
type: string
started_at:
type: string
format: datetime
ended_at:
type: string
format: datetime
duration:
type: integer
place:
type: object
'404':
description: visit not found
'401':
description: unauthorized
requestBody:
content:
application/json:
schema:
type: object
properties:
visit:
type: object
properties:
name:
type: string
place_id:
type: integer
status:
type: string
enum:
- suggested
- confirmed
- declined
delete:
summary: Delete visit
tags:
- Visits
parameters:
- name: id
in: path
required: true
description: Visit ID
schema:
type: integer
- name: Authorization
in: header
required: true
description: Bearer token
schema:
type: string
responses:
'204':
description: visit deleted
'404':
description: visit not found
'401':
description: unauthorized
"/api/v1/visits/{id}/possible_places":
get:
summary: Get possible places for visit
tags:
- Visits
parameters:
- name: id
in: path
required: true
description: Visit ID
schema:
type: integer
- name: Authorization
in: header
required: true
description: Bearer token
schema:
type: string
responses:
'200':
description: possible places found
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: integer
name:
type: string
latitude:
type: number
longitude:
type: number
city:
type: string
country:
type: string
'404':
description: visit not found
'401':
description: unauthorized
"/api/v1/visits/merge":
post:
summary: Merge visits
tags:
- Visits
parameters:
- name: Authorization
in: header
required: true
description: Bearer token
schema:
type: string
responses:
'200':
description: visits merged
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
status:
type: string
started_at:
type: string
format: datetime
ended_at:
type: string
format: datetime
duration:
type: integer
place:
type: object
'422':
description: invalid request
'401':
description: unauthorized
requestBody:
content:
application/json:
schema:
type: object
properties:
visit_ids:
type: array
items:
type: integer
minItems: 2
description: Array of visit IDs to merge (minimum 2)
required:
- visit_ids
"/api/v1/visits/bulk_update":
post:
summary: Bulk update visits
tags:
- Visits
parameters:
- name: Authorization
in: header
required: true
description: Bearer token
schema:
type: string
responses:
'200':
description: visits updated
content:
application/json:
schema:
type: object
properties:
message:
type: string
updated_count:
type: integer
'422':
description: invalid request
'401':
description: unauthorized
requestBody:
content:
application/json:
schema:
type: object
properties:
visit_ids:
type: array
items:
type: integer
description: Array of visit IDs to update
status:
type: string
enum:
- suggested
- confirmed
- declined
description: New status for the visits
required:
- visit_ids
- status
servers:
- url: http://{defaultHost}
variables: