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

9
lib/policy_service.ex Normal file
View File

@@ -0,0 +1,9 @@
defmodule PolicyService do
@moduledoc """
PolicyService keeps the contexts that define your domain
and business logic.
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
end

View File

@@ -0,0 +1,309 @@
defmodule PolicyService.Aggregates.CarPolicyApplication do
@moduledoc """
Aggregate for managing car insurance policy applications.
Lifecycle:
nil → :awaiting_quotes → :solicitation_sent → :issued
"""
defstruct [
:application_id,
:org_id,
:submitted_by,
:applicant_info,
:car_details,
:selected_providers,
:quotes,
:accepted_quote_id,
:accepted_provider_id,
:policy_number,
:state
]
alias PolicyService.Commands.Car.{
SubmitCarPolicyApplication,
RecordCarProviderQuote,
AcceptCarQuoteAndSolicit,
RecordCarPolicyIssued
}
alias PolicyService.Events.Car.{
CarPolicyApplicationSubmitted,
CarProviderQuoteReceived,
AllCarQuotesReceived,
CarQuoteAccepted,
CarSolicitationSent,
CarPolicyIssued,
CarQuoteRequestSent
}
# ---------------------------------------------------------------------------
# Submit — establishes org ownership
# ---------------------------------------------------------------------------
def execute(%__MODULE__{state: nil}, %SubmitCarPolicyApplication{} = cmd) do
with :ok <- validate_org(cmd.org_id),
:ok <- validate_user(cmd.submitted_by),
:ok <- validate_applicant(cmd.applicant_info),
:ok <- validate_car_details(cmd.car_details),
:ok <- validate_providers(cmd.selected_providers) do
quote_requests =
Enum.map(cmd.selected_providers, fn provider ->
%CarQuoteRequestSent{
application_id: cmd.application_id,
org_id: cmd.org_id,
provider_id: provider.id,
provider_email: provider.email,
applicant_info: cmd.applicant_info,
car_details: cmd.car_details,
requested_at: DateTime.utc_now()
}
end)
[
%CarPolicyApplicationSubmitted{
application_id: cmd.application_id,
org_id: cmd.org_id,
submitted_by: cmd.submitted_by,
applicant_info: cmd.applicant_info,
car_details: cmd.car_details,
selected_providers: cmd.selected_providers,
submitted_at: DateTime.utc_now()
}
| quote_requests
]
end
end
def execute(%__MODULE__{state: state}, %SubmitCarPolicyApplication{}) do
{:error, {:invalid_state, "cannot submit in state: #{state}"}}
end
# ---------------------------------------------------------------------------
# Record provider quote — external webhook, verify org
# ---------------------------------------------------------------------------
def execute(%__MODULE__{state: :awaiting_quotes} = agg, %RecordCarProviderQuote{} = cmd) do
with :ok <- verify_org(agg, cmd) do
if Map.has_key?(agg.quotes, cmd.provider_id) do
{:error, {:duplicate_quote, "quote from provider #{cmd.provider_id} already received"}}
else
quote_event = %CarProviderQuoteReceived{
application_id: cmd.application_id,
org_id: agg.org_id,
recorded_by: cmd.recorded_by,
provider_id: cmd.provider_id,
quote_id: cmd.quote_id,
premium: cmd.premium,
coverage_details: cmd.coverage_details,
valid_until: cmd.valid_until,
received_at: DateTime.utc_now()
}
new_quote_count = map_size(agg.quotes) + 1
if new_quote_count == length(agg.selected_providers) do
[
quote_event,
%AllCarQuotesReceived{
application_id: cmd.application_id,
org_id: agg.org_id,
quote_count: new_quote_count
}
]
else
quote_event
end
end
end
end
def execute(%__MODULE__{state: state}, %RecordCarProviderQuote{}) do
{:error, {:invalid_state, "cannot record quote in state: #{state}"}}
end
# ---------------------------------------------------------------------------
# Accept quote and solicit — internal user action, verify org
# ---------------------------------------------------------------------------
def execute(%__MODULE__{state: :awaiting_quotes} = agg, %AcceptCarQuoteAndSolicit{} = cmd) do
with :ok <- verify_org(agg, cmd) do
case find_quote(agg.quotes, cmd.quote_id) do
nil ->
{:error, {:quote_not_found, "quote #{cmd.quote_id} not found"}}
{provider_id, _quote} ->
[
%CarQuoteAccepted{
application_id: cmd.application_id,
org_id: agg.org_id,
accepted_by: cmd.accepted_by,
quote_id: cmd.quote_id,
provider_id: provider_id,
accepted_at: DateTime.utc_now()
},
%CarSolicitationSent{
application_id: cmd.application_id,
org_id: agg.org_id,
provider_id: provider_id,
quote_id: cmd.quote_id,
sent_at: DateTime.utc_now()
}
]
end
end
end
def execute(%__MODULE__{state: state}, %AcceptCarQuoteAndSolicit{}) do
{:error, {:invalid_state, "cannot accept quote in state: #{state}"}}
end
# ---------------------------------------------------------------------------
# Record policy issued — external or internal, verify org
# ---------------------------------------------------------------------------
def execute(%__MODULE__{state: :solicitation_sent} = agg, %RecordCarPolicyIssued{} = cmd) do
with :ok <- verify_org(agg, cmd) do
if cmd.provider_id != agg.accepted_provider_id do
{:error, {:provider_mismatch, "policy issued by unexpected provider"}}
else
%CarPolicyIssued{
application_id: cmd.application_id,
org_id: agg.org_id,
recorded_by: cmd.recorded_by,
policy_number: cmd.policy_number,
provider_id: cmd.provider_id,
effective_date: cmd.effective_date,
expiry_date: cmd.expiry_date,
issued_at: DateTime.utc_now()
}
end
end
end
def execute(%__MODULE__{state: state}, %RecordCarPolicyIssued{}) do
{:error, {:invalid_state, "cannot record policy in state: #{state}"}}
end
# ---------------------------------------------------------------------------
# Apply events
# ---------------------------------------------------------------------------
def apply(%__MODULE__{} = agg, %CarPolicyApplicationSubmitted{} = e) do
%__MODULE__{
agg
| application_id: e.application_id,
org_id: e.org_id,
submitted_by: e.submitted_by,
applicant_info: e.applicant_info,
car_details: e.car_details,
selected_providers: e.selected_providers,
quotes: %{},
state: :awaiting_quotes
}
end
def apply(%__MODULE__{} = agg, %CarQuoteRequestSent{}), do: agg
def apply(%__MODULE__{} = agg, %CarProviderQuoteReceived{} = e) do
quote_data = %{
quote_id: e.quote_id,
premium: e.premium,
coverage_details: e.coverage_details,
valid_until: e.valid_until
}
%__MODULE__{agg | quotes: Map.put(agg.quotes, e.provider_id, quote_data)}
end
def apply(%__MODULE__{} = agg, %AllCarQuotesReceived{}), do: agg
def apply(%__MODULE__{} = agg, %CarQuoteAccepted{} = e) do
%__MODULE__{agg | accepted_quote_id: e.quote_id, accepted_provider_id: e.provider_id}
end
def apply(%__MODULE__{} = agg, %CarSolicitationSent{}) do
%__MODULE__{agg | state: :solicitation_sent}
end
def apply(%__MODULE__{} = agg, %CarPolicyIssued{} = e) do
%__MODULE__{agg | policy_number: e.policy_number, state: :issued}
end
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
defp verify_org(%__MODULE__{org_id: org_id}, %{org_id: cmd_org_id}) do
if org_id == cmd_org_id,
do: :ok,
else: {:error, :org_mismatch}
end
defp validate_org(org_id) when is_binary(org_id) and byte_size(org_id) > 0, do: :ok
defp validate_org(_), do: {:error, :missing_org_id}
defp validate_user(user_id) when is_binary(user_id) and byte_size(user_id) > 0, do: :ok
defp validate_user(_), do: {:error, :missing_user_id}
defp validate_applicant(%{name: name, date_of_birth: dob, document_id: doc})
when is_binary(name) and is_binary(doc),
do: :ok
defp validate_applicant(_), do: {:error, :invalid_applicant_info}
@valid_use_types ~w(private commercial bus taxi school)a
@valid_car_types ~w(sedan suv hatchback coupe convertible pickup van minivan truck)a
defp validate_car_details(%{
plate: plate,
make: make,
model: model,
year: year,
car_value: car_value,
use_type: use_type,
car_type: car_type,
chassis_number: chassis_number,
engine_number: engine_number
})
when is_binary(plate) and is_binary(make) and is_binary(model) and
is_integer(year) and is_number(car_value) and car_value > 0 and
is_binary(chassis_number) and is_binary(engine_number) do
current_year = Date.utc_today().year
cond do
year < 1886 ->
{:error, :invalid_car_year}
year > current_year + 1 ->
{:error, :invalid_car_year}
use_type not in @valid_use_types ->
{:error, {:invalid_use_type, "must be one of: #{inspect(@valid_use_types)}"}}
car_type not in @valid_car_types ->
{:error, {:invalid_car_type, "must be one of: #{inspect(@valid_car_types)}"}}
byte_size(chassis_number) == 0 ->
{:error, :missing_chassis_number}
byte_size(engine_number) == 0 ->
{:error, :missing_engine_number}
true ->
:ok
end
end
defp validate_car_details(_), do: {:error, :invalid_car_details}
defp validate_providers(providers)
when is_list(providers) and length(providers) > 0,
do: :ok
defp validate_providers(_), do: {:error, :no_providers_selected}
defp find_quote(quotes, quote_id) do
Enum.find(quotes, fn {_provider_id, q} -> q.quote_id == quote_id end)
end
end

View File

@@ -0,0 +1,33 @@
defmodule PolicyService.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
PolicyService.CommandedApp,
PolicyService.Handlers.QuoteRequestHandler,
PolicyServiceWeb.Telemetry,
PolicyService.Repo,
{DNSCluster, query: Application.get_env(:policy_service, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: PolicyService.PubSub},
PolicyServiceWeb.Endpoint
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: PolicyService.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
@impl true
def config_change(changed, _new, removed) do
PolicyServiceWeb.Endpoint.config_change(changed, removed)
:ok
end
end

View File

@@ -0,0 +1,23 @@
defmodule PolicyService.Router do
use Commanded.Commands.Router
alias PolicyService.Commands.Car
alias PolicyService.Aggregates
dispatch(
[
Car.SubmitCarPolicyApplication,
Car.RecordCarProviderQuote,
Car.AcceptCarQuoteAndSolicit,
Car.RecordCarPolicyIssued
],
to: PolicyService.Aggregates.CarPolicyApplication,
identity: :application_id
)
end
defmodule PolicyService.CommandedApp do
use Commanded.Application,
otp_app: :policy_service
router(PolicyService.Router)
end

View File

@@ -0,0 +1,39 @@
defmodule PolicyService.Commands.Car.SubmitCarPolicyApplication do
defstruct [
:application_id,
:org_id,
:submitted_by,
:applicant_info,
:car_details,
:selected_providers
]
end
defmodule PolicyService.Commands.Car.RecordCarProviderQuote do
defstruct [
:application_id,
:org_id,
:recorded_by,
:provider_id,
:quote_id,
:premium,
:coverage_details,
:valid_until
]
end
defmodule PolicyService.Commands.Car.AcceptCarQuoteAndSolicit do
defstruct [:application_id, :org_id, :accepted_by, :quote_id]
end
defmodule PolicyService.Commands.Car.RecordCarPolicyIssued do
defstruct [
:application_id,
:org_id,
:recorded_by,
:policy_number,
:provider_id,
:effective_date,
:expiry_date
]
end

View File

@@ -0,0 +1,16 @@
defmodule PolicyService.Common.CarInfo do
use ExConstructor
@derive Jason.Encoder
defstruct [
:plate,
:make,
:model,
:year,
:car_value,
:use_type,
:car_type,
:chassis_number,
:engine_number
]
end

View File

@@ -0,0 +1,6 @@
defmodule PolicyService.Common.ClientInfo do
use ExConstructor
@derive Jason.Encoder
defstruct [:first_name, :last_name, :birth_date, :gender, :email, :phone, :user_id]
end

View File

@@ -0,0 +1,3 @@
defmodule PolicyService.EventStore do
use EventStore, otp_app: :policy_service
end

View File

@@ -0,0 +1,69 @@
defmodule PolicyService.Events.Car.CarPolicyApplicationSubmitted do
@derive Jason.Encoder
defstruct [
:application_id,
:org_id,
:submitted_by,
:applicant_info,
:car_details,
:selected_providers,
:submitted_at
]
end
defmodule PolicyService.Events.Car.CarQuoteRequestSent do
@derive Jason.Encoder
defstruct [
:application_id,
:org_id,
:provider_id,
:provider_email,
:applicant_info,
:car_details,
:requested_at
]
end
defmodule PolicyService.Events.Car.CarProviderQuoteReceived do
@derive Jason.Encoder
defstruct [
:application_id,
:org_id,
:recorded_by,
:provider_id,
:quote_id,
:premium,
:coverage_details,
:valid_until,
:received_at
]
end
defmodule PolicyService.Events.Car.AllCarQuotesReceived do
@derive Jason.Encoder
defstruct [:application_id, :org_id, :quote_count]
end
defmodule PolicyService.Events.Car.CarQuoteAccepted do
@derive Jason.Encoder
defstruct [:application_id, :org_id, :accepted_by, :quote_id, :provider_id, :accepted_at]
end
defmodule PolicyService.Events.Car.CarSolicitationSent do
@derive Jason.Encoder
defstruct [:application_id, :org_id, :provider_id, :quote_id, :sent_at]
end
defmodule PolicyService.Events.Car.CarPolicyIssued do
@derive Jason.Encoder
defstruct [
:application_id,
:org_id,
:recorded_by,
:policy_number,
:provider_id,
:effective_date,
:expiry_date,
:issued_at
]
end

View File

@@ -0,0 +1,21 @@
defmodule PolicyService.Handlers.QuoteRequestHandler do
use Commanded.Event.Handler,
application: PolicyService.CommandedApp,
name: __MODULE__
alias PolicyService.Events.Car.CarQuoteRequestSent
# alias PolicyService.Events.Life.LifeQuoteRequestSent
# alias PolicyService.Events.Fire.FireQuoteRequestSent
def handle(%CarQuoteRequestSent{} = e, _metadata) do
PolicyService.MessageBus.publish("carquote.requested", e)
end
# def handle(%LifeQuoteRequestSent{} = e, _metadata) do
# PolicyService.MessageBus.publish("quote.requested", e)
# end
# def handle(%FireQuoteRequestSent{} = e, _metadata) do
# PolicyService.MessageBus.publish("quote.requested", e)
# end
end

View File

@@ -0,0 +1,22 @@
defmodule PolicyService.MessageBus do
use AMQP
def publish(routing_key, event) do
payload = Jason.encode!(event)
:ok =
AMQP.Basic.publish(channel(), "policy_service.events", routing_key, payload,
content_type: "application/json",
# survives RabbitMQ restart
persistent: true
)
end
defp channel do
{:ok, conn} = AMQP.Connection.open(amqp_url())
{:ok, chan} = AMQP.Channel.open(conn)
chan
end
defp amqp_url, do: Application.fetch_env!(:policy_service, :amqp_url)
end

View File

@@ -0,0 +1,13 @@
defmodule PolicyService.Repo do
use Ecto.Repo,
otp_app: :policy_service,
adapter: Ecto.Adapters.Postgres
@doc """
Dynamically loads the repository url from the
DATABASE_URL environment variable.
"""
def init(_, opts) do
{:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
end
end

63
lib/policy_service_web.ex Normal file
View File

@@ -0,0 +1,63 @@
defmodule PolicyServiceWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, components, channels, and so on.
This can be used in your application as:
use PolicyServiceWeb, :controller
use PolicyServiceWeb, :html
The definitions below will be executed for every controller,
component, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define additional modules and import
those modules here.
"""
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
def router do
quote do
use Phoenix.Router, helpers: false
# Import common connection and controller functions to use in pipelines
import Plug.Conn
import Phoenix.Controller
end
end
def channel do
quote do
use Phoenix.Channel
end
end
def controller do
quote do
use Phoenix.Controller, formats: [:html, :json]
import Plug.Conn
unquote(verified_routes())
end
end
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: PolicyServiceWeb.Endpoint,
router: PolicyServiceWeb.Router,
statics: PolicyServiceWeb.static_paths()
end
end
@doc """
When used, dispatch to the appropriate controller/live_view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end

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