diff --git a/.rubocop.yml b/.rubocop.yml
index 07842b4..b1a53da 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,5 +1,8 @@
inherit_from: .rubocop_todo.yml
+inherit_gem:
+ rswag-specs: .rubocop_rspec_alias_config.yml
+
require:
- rubocop-rspec
- rubocop-rails
@@ -19,3 +22,10 @@ Style/Documentation:
Style/StringLiterals:
Enabled: true
EnforcedStyle: "double_quotes"
+
+# API Docs
+
+RSpec/NestedGroups:
+ Enabled: true
+ Exclude:
+ - 'spec/**/*_doc_spec.rb'
diff --git a/Gemfile b/Gemfile
index 950068c..e5ac2c1 100644
--- a/Gemfile
+++ b/Gemfile
@@ -58,6 +58,8 @@ gem "active_storage_validations" # Validate ActiveStorage attachments
gem "good_job" # Postgres-backed job queue
gem "mime-types"
gem "pdf-reader"
+gem "rswag-api" # Serves the generated Swagger documentation
+gem "rswag-ui" # Provides the Swagger UI interface
gem "sib-api-v3-sdk", require: false # Brevo (ex Sendinblue) API
group :development, :test do
@@ -68,6 +70,7 @@ group :development, :test do
gem "factory_bot_rails"
gem "pry-rails"
gem "rspec-rails"
+ gem "rswag-specs" # Allows API documentation via specs
end
group :test do
diff --git a/Gemfile.lock b/Gemfile.lock
index c3a8032..9d224f4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -220,6 +220,9 @@ GEM
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
json (2.8.2)
+ json-schema (5.1.1)
+ addressable (~> 2.8)
+ bigdecimal (~> 3.1)
language_server-protocol (3.17.0.3)
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
@@ -358,6 +361,17 @@ GEM
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.2)
+ rswag-api (2.16.0)
+ activesupport (>= 5.2, < 8.1)
+ railties (>= 5.2, < 8.1)
+ rswag-specs (2.16.0)
+ activesupport (>= 5.2, < 8.1)
+ json-schema (>= 2.2, < 6.0)
+ railties (>= 5.2, < 8.1)
+ rspec-core (>= 2.14)
+ rswag-ui (2.16.0)
+ actionpack (>= 5.2, < 8.1)
+ railties (>= 5.2, < 8.1)
rubocop (1.68.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
@@ -478,6 +492,9 @@ DEPENDENCIES
rails
rspec
rspec-rails
+ rswag-api
+ rswag-specs
+ rswag-ui
rubocop
rubocop-capybara
rubocop-factory_bot
diff --git a/README.md b/README.md
index 71d7b93..1b45298 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,7 @@ composants DSFR](https://github.com/betagouv/dsfr-view-components)
* Sentry pour monitorer et être alerté en cas d'erreur ;
* Matomo pour mesurer et comprendre l'usage via des analytics ;
* RSpec comme framework de tests ;
+* Rswag comme outil de documentation au format Swagger/ OpenAPI de l'API à travers des tests ;
* Cucumber et Capybara pour les tests BDD ;
* Rubocop (RSpec et Rails) pour le linting ;
* Docker pour avoir un environnement de développement.
@@ -35,27 +36,8 @@ Les fichiers devis sont traités par le `QuoteChecksController` qui les envoient
## API
-via header `Accept` à la valeur `application/json` pour forcer le retour au format JSON et non classique HTML
-
-- GET `/profils` pour lister les profils disponibles
-- POST `/[:profil]/devis/verifier` avec paramètre `quote_file` contenant le fichier
-le type de retour avec erreurs retournées est:
-```
-{
- "valid": false,
- "errors": [
- "file_reading_error",
- "devis_manquant",
- "pro_raison_sociale_manquant",
- "pro_forme_juridique_manquant",
- "tva_manquant",
- "capital_manquant",
- "siret_manquant",
- "client_prenom_manquant",
- "client_nom_manquant"
- ]
-}
-```
+- au format REST JSON
+- voir fichier de documentation de l'API au format OpenAPI Swagger et interface bac à sable interractif sur `/api-docs`
## Démarrage
diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb
index 6c374e2..adb4e26 100644
--- a/app/controllers/api/v1/profiles_controller.rb
+++ b/app/controllers/api/v1/profiles_controller.rb
@@ -5,7 +5,7 @@ module V1
# Controller for Profiles API
class ProfilesController < BaseController
def index
- render json: QuoteCheck::PROFILES
+ render json: { data: QuoteCheck::PROFILES }
end
end
end
diff --git a/app/controllers/api/v1/quote_checks_controller.rb b/app/controllers/api/v1/quote_checks_controller.rb
index 44e5245..f9ad834 100644
--- a/app/controllers/api/v1/quote_checks_controller.rb
+++ b/app/controllers/api/v1/quote_checks_controller.rb
@@ -7,6 +7,7 @@ class QuoteChecksController < BaseController
before_action :quote_check, only: %i[show]
def show
+ # Force to use async way by using show to get other fields
render json: quote_check_json
end
@@ -23,11 +24,13 @@ def create
upload_file.tempfile, upload_file.original_filename, quote_check_params[:profile]
)
@quote_check = quote_check_service.quote_check
- @quote_check = quote_check_service.check # Might be time consuming, TODO: move to background job is needed
+
+ # @quote_check = quote_check_service.check # Might be time consuming, TODO: move to background job is needed
+ QuoteCheckCheckJob.perform_later(@quote_check.id)
QuoteCheckMailer.created(@quote_check).deliver_later
- render json: quote_check_json(@quote_check)
+ render json: quote_check_json(@quote_check), status: :created
end
# rubocop:enable Metrics/MethodLength
@@ -38,20 +41,28 @@ def quote_check
end
def quote_check_params
- params.require(:quote_check).permit(:file, :profile)
+ params.permit(:file, :profile)
end
+ # rubocop:disable Metrics/MethodLength
def quote_check_json(quote_check_provided = nil)
object = quote_check_provided || quote_check
- object.attributes.merge({
- status: object.status,
- valid: object.quote_valid?,
- errors: object.validation_errors,
- error_messages: object.validation_errors&.index_with do |error_key|
- I18n.t("quote_validator.errors.#{error_key}")
- end
- })
+ json_hash = object.attributes.merge({ # Warning: attributes has stringifed keys, so use it too
+ "status" => object.status,
+ "valid" => object.quote_valid?,
+ "errors" => object.validation_errors,
+ "error_messages" => object.validation_errors&.index_with do |error_key|
+ I18n.t("quote_validator.errors.#{error_key}")
+ end
+ })
+ return json_hash if Rails.env.development?
+
+ json_hash.slice(
+ "id", "status", "profile",
+ "valid", "errors", "error_messages"
+ )
end
+ # rubocop:enable Metrics/MethodLength
end
end
end
diff --git a/app/jobs/quote_check_check_job.rb b/app/jobs/quote_check_check_job.rb
index 6dbbdb2..8686de0 100644
--- a/app/jobs/quote_check_check_job.rb
+++ b/app/jobs/quote_check_check_job.rb
@@ -6,6 +6,6 @@ class QuoteCheckCheckJob < ApplicationJob
def perform(quote_check_id)
quote_check = QuoteCheck.find(quote_check_id)
- QuoteCheckCheckService.new(quote_check).check
+ QuoteCheckService.new(quote_check).check
end
end
diff --git a/app/models/quote_check.rb b/app/models/quote_check.rb
index 4bd1938..e07e5e6 100644
--- a/app/models/quote_check.rb
+++ b/app/models/quote_check.rb
@@ -4,6 +4,8 @@
class QuoteCheck < ApplicationRecord
belongs_to :file, class_name: "QuoteFile"
+ STATUSES = %w[pending valid invalid].freeze
+
PROFILES = %w[artisan particulier mandataire conseiller].freeze
validates :profile, presence: true, inclusion: { in: PROFILES }
diff --git a/app/services/quote_check_service.rb b/app/services/quote_check_service.rb
index b98e78d..388ae75 100644
--- a/app/services/quote_check_service.rb
+++ b/app/services/quote_check_service.rb
@@ -4,7 +4,7 @@
class QuoteCheckService
attr_reader :quote_check
- def initialize(tempfile_or_quote_check, filename, profile)
+ def initialize(tempfile_or_quote_check, filename = nil, profile = nil)
@quote_check = if tempfile_or_quote_check.is_a?(QuoteCheck)
tempfile_or_quote_check
else
diff --git a/config/application.rb b/config/application.rb
index 723de61..5b618d5 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -52,5 +52,9 @@ class Application < Rails::Application
config.action_mailer.default_url_options = { host: ENV.fetch("APPLICATION_HOST", nil) }
config.application_name = "Mon Devis Sans Oublis"
+
+ config.openapi_file = lambda { |version|
+ "#{config.application_name.parameterize}_api_#{version.downcase}_swagger.yaml"
+ }
end
end
diff --git a/config/initializers/rswag_api.rb b/config/initializers/rswag_api.rb
new file mode 100644
index 0000000..3f23545
--- /dev/null
+++ b/config/initializers/rswag_api.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+Rswag::Api.configure do |config|
+ # Specify a root folder where Swagger JSON files are located
+ # This is used by the Swagger middleware to serve requests for API descriptions
+ # NOTE: If you're using rswag-specs to generate Swagger, you'll need to ensure
+ # that it's configured to generate files in the same folder
+ config.openapi_root = Rails.root.join("swagger").to_s
+
+ # Inject a lambda function to alter the returned Swagger prior to serialization
+ # The function will have access to the rack env for the current request
+ # For example, you could leverage this to dynamically assign the "host" property
+ #
+ # config.swagger_filter = lambda { |swagger, env| swagger['host'] = env['HTTP_HOST'] }
+end
diff --git a/config/initializers/rswag_ui.rb b/config/initializers/rswag_ui.rb
new file mode 100644
index 0000000..7eb3cec
--- /dev/null
+++ b/config/initializers/rswag_ui.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+Rswag::Ui.configure do |config|
+ # List the Swagger endpoints that you want to be documented through the
+ # swagger-ui. The first parameter is the path (absolute or relative to the UI
+ # host) to the corresponding endpoint and the second is a title that will be
+ # displayed in the document selector.
+ # NOTE: If you're using rspec-api to expose Swagger files
+ # (under openapi_root) as JSON or YAML endpoints, then the list below should
+ # correspond to the relative paths for those endpoints.
+
+ config.swagger_endpoint "/api-docs/v1/#{Rails.application.config.openapi_file.call('v1')}",
+ "#{Rails.application.config.application_name} API V1 Documentation"
+
+ # Add Basic Auth in case your API is private
+ # config.basic_auth_enabled = true
+ # config.basic_auth_credentials 'username', 'password'
+end
diff --git a/config/routes/api.rb b/config/routes/api.rb
index 51c86a2..94fcdf1 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -7,4 +7,7 @@
resources :quote_checks, only: %i[create show]
end
end
+
+ mount Rswag::Api::Engine => "/api-docs"
+ mount Rswag::Ui::Engine => "/api-docs"
end
diff --git a/spec/requests/api/v1/profiles_doc_spec.rb b/spec/requests/api/v1/profiles_doc_spec.rb
new file mode 100644
index 0000000..caf2e7f
--- /dev/null
+++ b/spec/requests/api/v1/profiles_doc_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "swagger_helper"
+
+describe "Profiles API" do
+ path "/profiles" do
+ get "Récupérer les profils disponibles" do
+ tags "Profils"
+ produces "application/json"
+
+ response "200", "liste des profiles" do
+ schema type: :object,
+ properties: {
+ data: {
+ type: "array",
+ data: { type: "#/components/schemas/profile" }
+ }
+ },
+ required: ["data"]
+ run_test!
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v1/profiles_spec.rb b/spec/requests/api/v1/profiles_spec.rb
index 9d5cec3..b7d9688 100644
--- a/spec/requests/api/v1/profiles_spec.rb
+++ b/spec/requests/api/v1/profiles_spec.rb
@@ -14,7 +14,7 @@
it "returns a complete response" do
get api_v1_profiles_url
- expect(json).to include(*QuoteCheck::PROFILES)
+ expect(json.fetch("data")).to include(*QuoteCheck::PROFILES)
end
end
end
diff --git a/spec/requests/api/v1/quote_checks_doc_spec.rb b/spec/requests/api/v1/quote_checks_doc_spec.rb
new file mode 100644
index 0000000..8bbbafc
--- /dev/null
+++ b/spec/requests/api/v1/quote_checks_doc_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require "swagger_helper"
+
+describe "Devis API" do
+ path "/quote_checks" do
+ # TODO: i18n?
+ post "Téléverser un devis" do
+ tags "Devis"
+ # TODO: security [ basic_auth: [] ]
+ consumes "multipart/form-data"
+ produces "application/json"
+
+ parameter name: :file, in: :formData, schema: {
+ type: :string,
+ format: :binary
+ }, required: true
+ parameter name: :profile, in: :formData, schema: {
+ "$ref" => "#/components/schemas/profile"
+ }, required: true
+
+ response "201", "Devis téléversé" do
+ schema "$ref" => "#/components/schemas/quote_check"
+ description "Au retour le devis a été téléversé avec succès.
+Mais vérifiez selon le statut si le devis a été déjà analysé ou non.
+Il peut contenir des erreurs dès le téléversement.
+Si le statut est 'pending', cela signifie que l'analyse est encore en cours.
+Et qu'il faut boucler sur l'appel /quote_check/:id pour récupérer le devis à jour.".gsub("\n", "
")
+
+ let(:file) { fixture_file_upload("quote_files/Devis_test.pdf") }
+ let(:profile) { "artisan" }
+
+ run_test!
+ end
+
+ response "422", "invalid request" do
+ let(:file) { fixture_file_upload("quote_files/Devis_test.pdf") }
+ let(:profile) { "blabla" }
+
+ run_test!
+ end
+ end
+ end
+
+ path "/quote_checks/{id}" do
+ get "Récupérer un Devis" do
+ tags "Devis"
+ consumes "application/json"
+ produces "application/json"
+ parameter name: :id, in: :path, type: :string, required: true
+
+ response "200", "Devis trouvé" do
+ schema "$ref" => "#/components/schemas/quote_check"
+
+ let(:id) { create(:quote_check).id }
+
+ run_test!
+ end
+
+ response "404", "Devis non trouvé" do
+ let(:id) { SecureRandom.uuid }
+
+ run_test!
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/v1/quote_checks_spec.rb b/spec/requests/api/v1/quote_checks_spec.rb
index 307f03a..9c9c4da 100644
--- a/spec/requests/api/v1/quote_checks_spec.rb
+++ b/spec/requests/api/v1/quote_checks_spec.rb
@@ -4,6 +4,8 @@
require "rails_helper"
RSpec.describe "/api/v1/quote_checks" do
+ let(:json) { response.parsed_body }
+
describe "POST /api/v1/quote_checks" do
let(:file) { fixture_file_upload("quote_files/Devis_test.pdf") }
let(:quote_check_params) do
@@ -12,52 +14,57 @@
profile: "artisan"
}
end
- let(:json) { response.parsed_body }
it "returns a successful response" do
- post api_v1_quote_checks_url, params: { quote_check: quote_check_params }
+ post api_v1_quote_checks_url, params: quote_check_params
expect(response).to be_successful
end
- # rubocop:disable RSpec/MultipleExpectations
- it "returns a complete response" do
- post api_v1_quote_checks_url, params: { quote_check: quote_check_params }
-
- expect(json.fetch("status")).to eq("invalid")
-
- expect(json.fetch("validation_errors")).to include("devis_manquant")
- expect(json.fetch("validation_errors")).not_to include("siret_manquant")
+ it "returns a created response" do
+ post api_v1_quote_checks_url, params: quote_check_params
+ expect(response).to have_http_status(:created)
+ end
- expect(json.dig("read_attributes", "pro", "siret")).to eq("12345678900000")
+ it "returns a pending treatment response" do
+ post api_v1_quote_checks_url, params: quote_check_params
+ expect(json.fetch("status")).to eq("pending")
end
- # rubocop:enable RSpec/MultipleExpectations
it "creates a QuoteCheck" do
expect do
- post api_v1_quote_checks_url, params: { quote_check: quote_check_params }
+ post api_v1_quote_checks_url, params: quote_check_params
end.to change(QuoteCheck, :count).by(1)
end
-
- context "with invalid file type" do
- let(:file) { fixture_file_upload("quote_files/Devis_test.png") }
-
- # rubocop:disable RSpec/MultipleExpectations
- it "returns a direct error response" do
- post api_v1_quote_checks_url, params: { quote_check: quote_check_params }
- expect(json.fetch("status")).to eq("invalid")
- expect(json.fetch("validation_errors")).to include("unsupported_file_format")
- end
- # rubocop:enable RSpec/MultipleExpectations
- end
end
describe "GET /api/v1/quote_checks/:id" do
let(:quote_file) { create(:quote_file) }
let(:quote_check) { create(:quote_check, file: quote_file) }
+ before do
+ QuoteCheckCheckJob.new.perform(quote_check.id)
+ end
+
+ # rubocop:disable RSpec/MultipleExpectations
it "renders a successful response" do
get api_v1_quote_check_url(quote_check), as: :json
expect(response).to be_successful
+ expect(json.fetch("status")).to eq("invalid")
+ end
+ # rubocop:enable RSpec/MultipleExpectations
+
+ context "with invalid file type" do
+ let(:file) { Rails.root.join("spec/fixtures/files/quote_files/Devis_test.png").open }
+ let(:quote_file) { create(:quote_file, file: file) }
+
+ # rubocop:disable RSpec/MultipleExpectations
+ it "returns a direct error response" do
+ get api_v1_quote_check_url(quote_check), as: :json
+ expect(response).to be_successful
+ expect(json.fetch("status")).to eq("invalid")
+ expect(json.fetch("errors")).to include("file_reading_error")
+ end
+ # rubocop:enable RSpec/MultipleExpectations
end
end
end
diff --git a/spec/swagger_helper.rb b/spec/swagger_helper.rb
new file mode 100644
index 0000000..9f6d8a8
--- /dev/null
+++ b/spec/swagger_helper.rb
@@ -0,0 +1,130 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+# Via Rswag gems
+RSpec.configure do |config|
+ # Specify a root folder where Swagger JSON files are generated
+ # NOTE: If you're using the rswag-api to serve API descriptions, you'll need
+ # to ensure that it's configured to serve Swagger from the same folder
+ config.openapi_root = Rails.root.join("swagger").to_s # TODO: doc
+
+ # Define one or more Swagger documents and provide global metadata for each one
+ # When you run the 'rswag:specs:swaggerize' rake task, the complete Swagger will
+ # be generated at the provided relative path under openapi_root
+ # By default, the operations defined in spec files are added to the first
+ # document below. You can override this behavior by adding a openapi_spec tag to the
+ # the root example_group in your specs, e.g. describe '...', openapi_spec: 'v2/swagger.json'
+ config.openapi_specs = {
+ "v1/#{Rails.application.config.openapi_file.call('v1')}" => {
+ openapi: "3.0.1",
+ info: {
+ title: "#{Rails.application.config.application_name} API V1",
+ version: "v1"
+ },
+ paths: {},
+ components: {
+ # TODO
+ # securitySchemes: {
+ # basic_auth: {
+ # type: :http,
+ # scheme: :basic
+ # },
+ # api_key: {
+ # type: :apiKey,
+ # name: "api_key",
+ # in: :query
+ # }
+ # },
+ schemas: {
+ profile: {
+ type: :string,
+ enum: QuoteCheck::PROFILES
+ },
+ quote_check_status: {
+ type: :string,
+ enum: QuoteCheck::STATUSES,
+ description: {
+ "pending" => "analyse en cours",
+ "valid" => "valide",
+ "invalid" => "invalide"
+ }.slice(*QuoteCheck::STATUSES).map { |status, description| "#{status}: #{description}" }.join(" | ")
+ },
+ quote_check_error: {
+ type: :string,
+ # enum: QuoteCheck::ERRORS, # TODO
+ description: "code d'erreur"
+ },
+ quote_check: {
+ type: "object",
+ properties: {
+ id: {
+ type: :string,
+ description: "UUID unique"
+ },
+ status: { type: :string },
+ profile: { "$ref" => "#/components/schemas/profile" },
+ valid: { type: :boolean, nullable: true },
+ errors: {
+ type: :array,
+ items: { "$ref" => "#/components/schemas/quote_check_error" },
+ description: "liste des erreurs dans ordre à afficher",
+ nullable: true
+ },
+ error_messages: {
+ type: :object,
+ additionalProperties: {
+ type: :string,
+ description: "code d'erreur => message"
+ },
+ nullable: true
+ }
+ },
+ required: %w[id status profile]
+ }
+ }
+ },
+ servers: [ # Swagger reccomends to have path version listed inside server URLs
+ {
+ url: "https://api.staging.mon-devis-sans-oublis.beta.gouv.fr/api/v1",
+ description: "Staging server"
+ },
+ {
+ url: "https://api.mon-devis-sans-oublis.beta.gouv.fr/api/v1",
+ description: "Production server"
+ },
+ {
+ url: "http://localhost:3000/api/v1",
+ description: "Development server"
+ },
+ if ENV.key?("APPLICATION_HOST") # current host
+ {
+ url: "http#{Rails.env.development? ? '' : 's'}://#{ENV.fetch('APPLICATION_HOST')}",
+ variables: {
+ defaultHost: {
+ default: ENV.fetch("APPLICATION_HOST", "localhost:3000")
+ }
+ }
+ }
+ end,
+ { # example host
+ url: "http#{Rails.env.development? ? '' : 's'}://{defaultHost}",
+ variables: {
+ defaultHost: {
+ default: ENV.fetch("APPLICATION_HOST", "localhost:3000")
+ }
+ }
+ }
+ ].compact.uniq { |server| server[:url] }
+ }
+ }
+
+ # Specify the format of the output Swagger file when running 'rswag:specs:swaggerize'.
+ # The openapi_specs configuration option has the filename including format in
+ # the key, this may want to be changed to avoid putting yaml in json files.
+ # Defaults to json. Accepts ':json' and ':yaml'.
+ config.openapi_format = :yaml
+ # TODO: config.swagger_format = :json
+
+ # TODO: config.openapi_strict_schema_validation = true
+end
diff --git a/swagger/v1/mon-devis-sans-oublis_api_v1_swagger.yaml b/swagger/v1/mon-devis-sans-oublis_api_v1_swagger.yaml
new file mode 100644
index 0000000..19c94bb
--- /dev/null
+++ b/swagger/v1/mon-devis-sans-oublis_api_v1_swagger.yaml
@@ -0,0 +1,131 @@
+---
+openapi: 3.0.1
+info:
+ title: Mon Devis Sans Oublis API V1
+ version: v1
+paths:
+ "/profiles":
+ get:
+ summary: Récupérer les profils disponibles
+ tags:
+ - Profils
+ responses:
+ '200':
+ description: liste des profiles
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ data:
+ type: array
+ data:
+ type: "#/components/schemas/profile"
+ required:
+ - data
+ "/quote_checks":
+ post:
+ summary: Téléverser un devis
+ tags:
+ - Devis
+ parameters: []
+ description: Au retour le devis a été téléversé avec succès.
Mais vérifiez
+ selon le statut si le devis a été déjà analysé ou non.
Il peut contenir
+ des erreurs dès le téléversement.
Si le statut est 'pending', cela signifie
+ que l'analyse est encore en cours.
Et qu'il faut boucler sur l'appel /quote_check/:id
+ pour récupérer le devis à jour.
+ responses:
+ '201':
+ description: Devis téléversé
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/quote_check"
+ '422':
+ description: invalid request
+ requestBody:
+ content:
+ multipart/form-data:
+ schema:
+ type: string
+ format: binary
+ required: true
+ "/quote_checks/{id}":
+ get:
+ summary: Récupérer un Devis
+ tags:
+ - Devis
+ parameters:
+ - name: id
+ in: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: Devis trouvé
+ content:
+ application/json:
+ schema:
+ "$ref": "#/components/schemas/quote_check"
+ '404':
+ description: Devis non trouvé
+components:
+ schemas:
+ profile:
+ type: string
+ enum:
+ - artisan
+ - particulier
+ - mandataire
+ - conseiller
+ quote_check_status:
+ type: string
+ enum:
+ - pending
+ - valid
+ - invalid
+ description: 'pending: analyse en cours | valid: valide | invalid: invalide'
+ quote_check_error:
+ type: string
+ description: code d'erreur
+ quote_check:
+ type: object
+ properties:
+ id:
+ type: string
+ description: UUID unique
+ status:
+ type: string
+ profile:
+ "$ref": "#/components/schemas/profile"
+ valid:
+ type: boolean
+ nullable: true
+ errors:
+ type: array
+ items:
+ "$ref": "#/components/schemas/quote_check_error"
+ description: liste des erreurs dans ordre à afficher
+ nullable: true
+ error_messages:
+ type: object
+ additionalProperties:
+ type: string
+ description: code d'erreur => message
+ nullable: true
+ required:
+ - id
+ - status
+ - profile
+servers:
+- url: https://api.staging.mon-devis-sans-oublis.beta.gouv.fr/api/v1
+ description: Staging server
+- url: https://api.mon-devis-sans-oublis.beta.gouv.fr/api/v1
+ description: Production server
+- url: http://localhost:3000/api/v1
+ description: Development server
+- url: https://{defaultHost}
+ variables:
+ defaultHost:
+ default: localhost:3000