This commit is contained in:
Haim Kortovich
2026-03-05 11:30:08 -05:00
commit a52f049a29
46 changed files with 1938 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
defmodule PolicyServiceWeb.ApiSpec do
alias OpenApiSpex.{OpenApi, Info, Server}
alias OpenApiSpex.{Info, OpenApi, Paths, Server}
alias PolicyServiceWeb.{Endpoint, Router}
@behaviour OpenApi
@impl OpenApi
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
Server.from_endpoint(Endpoint)
],
info: %Info{
title: "Policy Service",
version: "1.0"
},
# Populate the paths from a phoenix router
paths: Paths.from_router(Router)
}
# Discover request/response schemas from path specs
|> OpenApiSpex.resolve_schema_modules()
end
end

View File

@@ -0,0 +1,85 @@
# lib/policy_service_web/controllers/car_policy_controller.ex
defmodule PolicyServiceWeb.CarPolicyController do
use PolicyServiceWeb, :controller
use OpenApiSpex.ControllerSpecs
alias OpenApiSpex.Schema
alias PolicyServiceWeb.Schemas.CarPolicy.{QuoteRequest, QuoteResponse}
alias PolicyService.Commands.Car.SubmitCarPolicyApplication
tags(["Car Policy"])
security([%{"bearerAuth" => []}])
operation(:request_quote,
summary: "Solicitar cotización de seguro de auto",
description: "Envía una solicitud de cotización a los proveedores seleccionados",
request_body: {"Quote request body", "application/json", QuoteRequest, required: true},
responses: [
created: {"Solicitud creada exitosamente", "application/json", QuoteResponse},
unprocessable_entity:
{"Error de validación", "application/json",
%Schema{
type: :object,
properties: %{
errors: %Schema{type: :object}
}
}}
]
)
def request_quote(conn, params) do
user = %{"id" => "test", "org_id" => "test"}
cmd = %SubmitCarPolicyApplication{
application_id: Ecto.UUID.generate(),
org_id: user["org_id"],
submitted_by: user["id"],
applicant_info: %{
name: params["applicant_info"]["name"],
date_of_birth: Date.from_iso8601!(params["applicant_info"]["date_of_birth"]),
document_id: params["applicant_info"]["document_id"]
},
car_details: %{
plate: params["car_details"]["plate"],
make: params["car_details"]["make"],
model: params["car_details"]["model"],
year: params["car_details"]["year"],
car_value: parse_number(params["car_details"]["car_value"]),
use_type: String.to_atom(params["car_details"]["use_type"]),
car_type: String.to_atom(params["car_details"]["car_type"]),
chassis_number: params["car_details"]["chassis_number"],
engine_number: params["car_details"]["engine_number"]
},
selected_providers:
Enum.map(params["selected_providers"], fn p ->
%{id: p["id"], email: p["email"]}
end)
}
case PolicyService.CommandedApp.dispatch(cmd) do
:ok ->
conn
|> put_status(:created)
|> json(%{
application_id: cmd.applicant_info,
status: "awaiting_quotes"
})
{:error, reason} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{errors: reason})
end
end
defp parse_number(val) when is_float(val), do: val
defp parse_number(val) when is_integer(val), do: val * 1.0
defp parse_number(val) when is_binary(val) do
case Float.parse(val) do
{f, _} -> f
:error -> raise "invalid number: #{val}"
end
end
end

View File

@@ -0,0 +1,21 @@
defmodule PolicyServiceWeb.ErrorJSON do
@moduledoc """
This module is invoked by your endpoint in case of errors on JSON requests.
See config/config.exs.
"""
# If you want to customize a particular status code,
# you may add your own clauses, such as:
#
# def render("500.json", _assigns) do
# %{errors: %{detail: "Internal Server Error"}}
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.json" becomes
# "Not Found".
def render(template, _assigns) do
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
end
end

View File

@@ -0,0 +1,49 @@
defmodule PolicyServiceWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :policy_service
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
@session_options [
store: :cookie,
key: "_policy_service_key",
signing_salt: "9eYllgTe",
same_site: "Lax"
]
# socket "/live", Phoenix.LiveView.Socket,
# websocket: [connect_info: [session: @session_options]],
# longpoll: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#
# When code reloading is disabled (e.g., in production),
# the `gzip` option is enabled to serve compressed
# static files generated by running `phx.digest`.
plug Plug.Static,
at: "/",
from: :policy_service,
gzip: not code_reloading?,
only: PolicyServiceWeb.static_paths(),
raise_on_missing_only: code_reloading?
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :policy_service
end
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug PolicyServiceWeb.Router
end

View File

@@ -0,0 +1,26 @@
defmodule PolicyServiceWeb.Router do
use PolicyServiceWeb, :router
pipeline :api do
plug OpenApiSpex.Plug.PutApiSpec, module: PolicyServiceWeb.ApiSpec
end
scope "/api" do
pipe_through [:api]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
scope "/v1" do
scope "/car-policies" do
post "/quotes", PolicyServiceWeb.CarPolicyController, :request_quote
end
end
end
# Swagger UI — only in dev
if Mix.env() == :dev do
scope "/swaggerui" do
get "/", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
end
end
end

View File

@@ -0,0 +1,114 @@
defmodule PolicyServiceWeb.Schemas.CarPolicy do
alias OpenApiSpex.Schema
defmodule ApplicantInfo do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "ApplicantInfo",
type: :object,
required: [:name, :date_of_birth, :document_id],
properties: %{
name: %Schema{type: :string, example: "Juan Pérez"},
date_of_birth: %Schema{type: :string, format: :date, example: "1985-06-15"},
document_id: %Schema{type: :string, example: "V-12345678"}
}
})
end
defmodule CarDetails do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "CarDetails",
type: :object,
required: [
:plate,
:make,
:model,
:year,
:car_value,
:use_type,
:car_type,
:chassis_number,
:engine_number
],
properties: %{
plate: %Schema{type: :string, example: "ABC-1234"},
make: %Schema{type: :string, example: "Toyota"},
model: %Schema{type: :string, example: "Corolla"},
year: %Schema{type: :integer, example: 2022},
car_value: %Schema{type: :number, example: 18000},
use_type: %Schema{
type: :string,
enum: ["private", "commercial", "bus", "taxi", "school"],
example: "private"
},
car_type: %Schema{
type: :string,
enum: [
"sedan",
"suv",
"hatchback",
"coupe",
"convertible",
"pickup",
"van",
"minivan",
"truck"
],
example: "sedan"
},
chassis_number: %Schema{type: :string, example: "9BWZZZ377VT004251"},
engine_number: %Schema{type: :string, example: "1NZ-FE-1234567"}
}
})
end
defmodule Provider do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Provider",
type: :object,
required: [:id, :email],
properties: %{
id: %Schema{type: :string, example: "provider-uuid"},
email: %Schema{type: :string, format: :email, example: "cotizaciones@aseguradora.com"}
}
})
end
defmodule QuoteRequest do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "QuoteRequest",
type: :object,
required: [:applicant_info, :car_details, :selected_providers],
properties: %{
applicant_info: ApplicantInfo,
car_details: CarDetails,
selected_providers: %Schema{
type: :array,
items: Provider,
minItems: 1,
example: [%{id: "provider-uuid", email: "cotizaciones@aseguradora.com"}]
}
}
})
end
defmodule QuoteResponse do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "QuoteResponse",
type: :object,
properties: %{
application_id: %Schema{type: :string, example: "550e8400-e29b-41d4-a716-446655440000"},
status: %Schema{type: :string, example: "awaiting_quotes"}
}
})
end
end

View File

@@ -0,0 +1,93 @@
defmodule PolicyServiceWeb.Telemetry do
use Supervisor
import Telemetry.Metrics
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
children = [
# Telemetry poller will execute the given period measurements
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
# Add reporters as children of your supervision tree.
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
]
Supervisor.init(children, strategy: :one_for_one)
end
def metrics do
[
# Phoenix Metrics
summary("phoenix.endpoint.start.system_time",
unit: {:native, :millisecond}
),
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.start.system_time",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.exception.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.socket_connected.duration",
unit: {:native, :millisecond}
),
sum("phoenix.socket_drain.count"),
summary("phoenix.channel_joined.duration",
unit: {:native, :millisecond}
),
summary("phoenix.channel_handled_in.duration",
tags: [:event],
unit: {:native, :millisecond}
),
# Database Metrics
summary("policy_service.repo.query.total_time",
unit: {:native, :millisecond},
description: "The sum of the other measurements"
),
summary("policy_service.repo.query.decode_time",
unit: {:native, :millisecond},
description: "The time spent decoding the data received from the database"
),
summary("policy_service.repo.query.query_time",
unit: {:native, :millisecond},
description: "The time spent executing the query"
),
summary("policy_service.repo.query.queue_time",
unit: {:native, :millisecond},
description: "The time spent waiting for a database connection"
),
summary("policy_service.repo.query.idle_time",
unit: {:native, :millisecond},
description:
"The time the connection spent waiting before being checked out for the query"
),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io")
]
end
defp periodic_measurements do
[
# A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above.
# {PolicyServiceWeb, :count_users, []}
]
end
end