This commit is contained in:
@@ -1,309 +1,34 @@
|
||||
defmodule PolicyService.Aggregates.CarPolicyApplication do
|
||||
@moduledoc """
|
||||
Aggregate for managing car insurance policy applications.
|
||||
use PolicyService.Aggregates.PolicyApplication,
|
||||
policy_type: "car",
|
||||
commands: PolicyService.Commands.CarPolicy
|
||||
|
||||
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
|
||||
@valid_use_types ~w(private commercial bus taxi school)
|
||||
@valid_car_types ~w(sedan suv hatchback coupe convertible pickup van minivan truck)
|
||||
|
||||
def validate_details(%{
|
||||
"plate" => plate,
|
||||
"make" => make,
|
||||
"model" => model,
|
||||
"year" => year,
|
||||
"car_value" => car_value,
|
||||
"use_type" => use_type,
|
||||
"car_type" => car_type,
|
||||
"chassis_number" => chassis,
|
||||
"engine_number" => engine
|
||||
})
|
||||
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) and is_binary(engine) do
|
||||
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
|
||||
year < 1886 or year > Date.utc_today().year + 1 -> {:error, :invalid_car_year}
|
||||
use_type not in @valid_use_types -> {:error, :invalid_use_type}
|
||||
car_type not in @valid_car_types -> {:error, :invalid_car_type}
|
||||
byte_size(chassis) == 0 -> {:error, :missing_chassis_number}
|
||||
byte_size(engine) == 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
|
||||
def validate_details(_), do: {:error, :invalid_car_details}
|
||||
end
|
||||
|
||||
11
lib/policy_service/aggregates/fire_policy_application.ex
Normal file
11
lib/policy_service/aggregates/fire_policy_application.ex
Normal file
@@ -0,0 +1,11 @@
|
||||
defmodule PolicyService.Aggregates.FirePolicyApplication do
|
||||
use PolicyService.Aggregates.PolicyApplication,
|
||||
policy_type: "fire",
|
||||
commands: PolicyService.Commands.FirePolicy
|
||||
|
||||
def validate_details(%{property_address: addr, property_value: val})
|
||||
when is_binary(addr) and byte_size(addr) > 0 and is_number(val) and val > 0,
|
||||
do: :ok
|
||||
|
||||
def validate_details(_), do: {:error, :invalid_fire_details}
|
||||
end
|
||||
284
lib/policy_service/aggregates/policy_application.ex
Normal file
284
lib/policy_service/aggregates/policy_application.ex
Normal file
@@ -0,0 +1,284 @@
|
||||
defmodule PolicyService.Aggregates.PolicyApplication do
|
||||
@moduledoc """
|
||||
Behaviour and __using__ macro for policy application aggregates.
|
||||
Each policy type implements validate_details/1 and declares its detail fields.
|
||||
|
||||
Usage:
|
||||
defmodule PolicyService.Aggregates.CarPolicyApplication do
|
||||
use PolicyService.Aggregates.PolicyApplication,
|
||||
policy_type: "car"
|
||||
end
|
||||
"""
|
||||
|
||||
@callback validate_details(map()) :: :ok | {:error, term()}
|
||||
|
||||
defmacro __using__(opts) do
|
||||
policy_type = Keyword.fetch!(opts, :policy_type)
|
||||
commands_module = Keyword.get(opts, :commands, PolicyService.Commands.Policy)
|
||||
|
||||
quote do
|
||||
@behaviour Commanded.Aggregates.Aggregate
|
||||
|
||||
alias unquote(commands_module).SubmitPolicyApplication
|
||||
alias unquote(commands_module).RecordProviderQuote
|
||||
alias unquote(commands_module).AcceptQuoteAndSolicit
|
||||
alias unquote(commands_module).RecordPolicyIssued
|
||||
|
||||
alias PolicyService.Events.Policy.{
|
||||
PolicyApplicationSubmitted,
|
||||
QuoteRequestSent,
|
||||
ProviderQuoteReceived,
|
||||
AllQuotesReceived,
|
||||
QuoteAccepted,
|
||||
SolicitationSent,
|
||||
PolicyIssued
|
||||
}
|
||||
|
||||
@policy_type unquote(policy_type)
|
||||
|
||||
defstruct [
|
||||
:id,
|
||||
:submitted_by,
|
||||
:applicant_info,
|
||||
:policy_details,
|
||||
:selected_providers,
|
||||
:accepted_quote_id,
|
||||
:accepted_plan_id,
|
||||
:accepted_provider_id,
|
||||
:solicitation_id,
|
||||
:policy_number,
|
||||
:effective_date,
|
||||
:expiry_date,
|
||||
:state,
|
||||
quotes: %{},
|
||||
pending_endorsements: %{}
|
||||
]
|
||||
|
||||
# ── Execute ────────────────────────────────────────────────────────────
|
||||
|
||||
@impl Commanded.Aggregates.Aggregate
|
||||
def execute(%__MODULE__{state: nil}, %SubmitPolicyApplication{} = cmd) do
|
||||
with :ok <- PolicyService.Aggregates.PolicyApplication.validate_policy_id(cmd.id),
|
||||
:ok <-
|
||||
PolicyService.Aggregates.PolicyApplication.validate_applicant(cmd.applicant_info),
|
||||
:ok <- validate_details(cmd.policy_details),
|
||||
:ok <-
|
||||
PolicyService.Aggregates.PolicyApplication.validate_providers(
|
||||
cmd.selected_providers
|
||||
) do
|
||||
quote_requests =
|
||||
Enum.map(cmd.selected_providers, fn provider ->
|
||||
%QuoteRequestSent{
|
||||
id: cmd.id,
|
||||
provider_id: provider.provider_id,
|
||||
provider_email: provider.email,
|
||||
applicant_info: cmd.applicant_info,
|
||||
policy_details: cmd.policy_details,
|
||||
requested_at: DateTime.utc_now()
|
||||
}
|
||||
end)
|
||||
|
||||
[
|
||||
%PolicyApplicationSubmitted{
|
||||
id: cmd.id,
|
||||
submitted_by: cmd.submitted_by,
|
||||
applicant_info: cmd.applicant_info,
|
||||
policy_details: cmd.policy_details,
|
||||
selected_providers: cmd.selected_providers,
|
||||
submitted_at: DateTime.utc_now()
|
||||
}
|
||||
| quote_requests
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def execute(%__MODULE__{state: state}, %SubmitPolicyApplication{}) do
|
||||
{:error, {:invalid_state, "cannot submit in state: #{state}"}}
|
||||
end
|
||||
|
||||
def execute(%__MODULE__{state: :awaiting_quotes} = agg, %RecordProviderQuote{} = cmd) do
|
||||
if Map.has_key?(agg.quotes, cmd.provider_id) do
|
||||
{:error, {:duplicate_quote, "quote from #{cmd.provider_id} already received"}}
|
||||
else
|
||||
quote_event = %ProviderQuoteReceived{
|
||||
id: cmd.id,
|
||||
recorded_by: cmd.recorded_by,
|
||||
provider_id: cmd.provider_id,
|
||||
quote_id: cmd.quote_id,
|
||||
valid_until: cmd.valid_until,
|
||||
plans: cmd.plans,
|
||||
received_at: DateTime.utc_now()
|
||||
}
|
||||
|
||||
new_quote_count = map_size(agg.quotes) + 1
|
||||
|
||||
if new_quote_count == length(agg.selected_providers) do
|
||||
[
|
||||
quote_event,
|
||||
%AllQuotesReceived{
|
||||
id: cmd.id,
|
||||
quote_count: new_quote_count
|
||||
}
|
||||
]
|
||||
else
|
||||
quote_event
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def execute(%__MODULE__{state: state}, %RecordProviderQuote{}) do
|
||||
{:error, {:invalid_state, "cannot record quote in state: #{state}"}}
|
||||
end
|
||||
|
||||
def execute(%__MODULE__{state: :awaiting_quotes}, %AcceptQuoteAndSolicit{}) do
|
||||
{:error, :no_quotes_received}
|
||||
end
|
||||
|
||||
def execute(%__MODULE__{state: state}, %AcceptQuoteAndSolicit{})
|
||||
when state not in [:quotes_received] do
|
||||
{:error, :invalid_state}
|
||||
end
|
||||
|
||||
def execute(%__MODULE__{} = agg, %AcceptQuoteAndSolicit{} = cmd) do
|
||||
with {:ok, quote} <-
|
||||
PolicyService.Aggregates.PolicyApplication.find_quote(agg, cmd.quote_id),
|
||||
{:ok, provider} <-
|
||||
PolicyService.Aggregates.PolicyApplication.find_provider(agg, quote.provider_id),
|
||||
{:ok, plan} <-
|
||||
PolicyService.Aggregates.PolicyApplication.find_plan(quote, cmd.plan_id) do
|
||||
%QuoteAccepted{
|
||||
id: agg.id,
|
||||
quote: quote,
|
||||
plan: plan,
|
||||
provider: provider,
|
||||
accepted_at: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def execute(%__MODULE__{state: :issued}, %RecordPolicyIssued{}),
|
||||
do: {:error, :already_issued}
|
||||
|
||||
def execute(%__MODULE__{} = agg, %RecordPolicyIssued{} = cmd) do
|
||||
%PolicyIssued{
|
||||
id: agg.id,
|
||||
policy_number: cmd.policy_number,
|
||||
effective_date: cmd.effective_date,
|
||||
expiry_date: cmd.expiry_date,
|
||||
issued_at: cmd.issued_at || DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
|
||||
# ── Apply ──────────────────────────────────────────────────────────────
|
||||
|
||||
@impl Commanded.Aggregates.Aggregate
|
||||
def apply(%__MODULE__{} = agg, %PolicyApplicationSubmitted{} = e) do
|
||||
%__MODULE__{
|
||||
agg
|
||||
| id: e.id,
|
||||
submitted_by: e.submitted_by,
|
||||
applicant_info: e.applicant_info,
|
||||
policy_details: e.policy_details,
|
||||
selected_providers: e.selected_providers,
|
||||
quotes: %{},
|
||||
state: :awaiting_quotes
|
||||
}
|
||||
end
|
||||
|
||||
def apply(%__MODULE__{} = agg, %QuoteRequestSent{}), do: agg
|
||||
|
||||
def apply(%__MODULE__{} = agg, %ProviderQuoteReceived{} = e) do
|
||||
quote_data = %{
|
||||
quote_id: e.quote_id,
|
||||
provider_id: e.provider_id,
|
||||
valid_until: e.valid_until,
|
||||
plans: e.plans || []
|
||||
}
|
||||
|
||||
%__MODULE__{agg | quotes: Map.put(agg.quotes, e.provider_id, quote_data)}
|
||||
end
|
||||
|
||||
def apply(%__MODULE__{} = agg, %AllQuotesReceived{}) do
|
||||
%__MODULE__{agg | state: :quotes_received}
|
||||
end
|
||||
|
||||
def apply(%__MODULE__{} = agg, %QuoteAccepted{} = e) do
|
||||
%__MODULE__{
|
||||
agg
|
||||
| accepted_quote_id: e.quote.quote_id,
|
||||
accepted_plan_id: e.plan.plan_id,
|
||||
accepted_provider_id: e.provider.provider_id,
|
||||
state: :solicitation_sent
|
||||
}
|
||||
end
|
||||
|
||||
def apply(%__MODULE__{} = agg, %SolicitationSent{} = e) do
|
||||
%__MODULE__{agg | solicitation_id: e.solicitation_id}
|
||||
end
|
||||
|
||||
def apply(%__MODULE__{} = agg, %PolicyIssued{} = e) do
|
||||
%__MODULE__{
|
||||
agg
|
||||
| policy_number: e.policy_number,
|
||||
effective_date: e.effective_date,
|
||||
expiry_date: e.expiry_date,
|
||||
state: :issued
|
||||
}
|
||||
end
|
||||
|
||||
# allow each aggregate to override any callback
|
||||
defoverridable execute: 2, apply: 2
|
||||
end
|
||||
end
|
||||
|
||||
def validate_policy_id(%PolicyService.Aggregates.PolicyId{policy_type: _}), do: :ok
|
||||
def validate_policy_id(_), do: {:error, :invalid_policy_id_format}
|
||||
|
||||
def validate_user(id) when is_binary(id) and byte_size(id) > 0, do: :ok
|
||||
def validate_user(_), do: {:error, :missing_user_id}
|
||||
|
||||
def validate_applicant(%{"name" => n, "date_of_birth" => _, "document_id" => d})
|
||||
when is_binary(n) and is_binary(d) and byte_size(n) > 0 and byte_size(d) > 0,
|
||||
do: :ok
|
||||
|
||||
# Match on string keys for Company
|
||||
def validate_applicant(%{
|
||||
"company_name" => c,
|
||||
"ruc" => r,
|
||||
"legal_rep_name" => rep,
|
||||
"legal_rep_document" => rd
|
||||
})
|
||||
when is_binary(c) and is_binary(r) and is_binary(rep) and is_binary(rd) and
|
||||
byte_size(c) > 0 and byte_size(r) > 0,
|
||||
do: :ok
|
||||
|
||||
def validate_applicant(_), do: {:error, :invalid_applicant_info}
|
||||
|
||||
def validate_providers(p) when is_list(p) and length(p) > 0, do: :ok
|
||||
def validate_providers(_), do: {:error, :no_providers_selected}
|
||||
|
||||
def find_quote(agg, quote_id) do
|
||||
case Enum.find(agg.quotes, fn {_, q} -> q.quote_id == quote_id end) do
|
||||
nil -> {:error, :quote_not_found}
|
||||
{_, quote} -> {:ok, quote}
|
||||
end
|
||||
end
|
||||
|
||||
def find_plan(quote, plan_id) do
|
||||
case Enum.find(quote.plans || [], fn p ->
|
||||
Map.get(p, :plan_id) == plan_id or Map.get(p, "plan_id") == plan_id
|
||||
end) do
|
||||
nil -> {:error, :plan_not_found}
|
||||
plan -> {:ok, plan}
|
||||
end
|
||||
end
|
||||
|
||||
def find_provider(agg, provider_id) do
|
||||
case Enum.find(agg.selected_providers || [], fn p ->
|
||||
Map.get(p, :provider_id) == provider_id
|
||||
end) do
|
||||
nil -> {:error, :provider_not_found}
|
||||
provider -> {:ok, provider}
|
||||
end
|
||||
end
|
||||
end
|
||||
48
lib/policy_service/aggregates/policy_id.ex
Normal file
48
lib/policy_service/aggregates/policy_id.ex
Normal file
@@ -0,0 +1,48 @@
|
||||
defmodule PolicyService.Aggregates.PolicyId do
|
||||
@derive Jason.Encoder
|
||||
defstruct [:org_id, :policy_type, :application_id]
|
||||
|
||||
def new(org_id, policy_type, application_id) do
|
||||
%__MODULE__{
|
||||
org_id: org_id,
|
||||
policy_type: policy_type,
|
||||
application_id: application_id
|
||||
}
|
||||
end
|
||||
|
||||
def parse(string) when is_binary(string) do
|
||||
case String.split(string, ":", parts: 3) do
|
||||
[org_id, policy_type, application_id] ->
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
org_id: org_id,
|
||||
policy_type: policy_type,
|
||||
application_id: application_id
|
||||
}}
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_policy_id}
|
||||
end
|
||||
end
|
||||
|
||||
def parse!(string) do
|
||||
case parse(string) do
|
||||
{:ok, id} -> id
|
||||
{:error, reason} -> raise ArgumentError, "invalid policy id #{inspect(string)}: #{reason}"
|
||||
end
|
||||
end
|
||||
|
||||
defimpl String.Chars do
|
||||
def to_string(%PolicyService.Aggregates.PolicyId{
|
||||
org_id: org_id,
|
||||
policy_type: policy_type,
|
||||
application_id: application_id
|
||||
}) do
|
||||
org_id <> ":" <> policy_type <> ":" <> application_id
|
||||
end
|
||||
end
|
||||
|
||||
defimpl Commanded.Serialization.JsonDecoder do
|
||||
def decode(id), do: id
|
||||
end
|
||||
end
|
||||
@@ -10,6 +10,10 @@ defmodule PolicyService.Application do
|
||||
children = [
|
||||
PolicyService.CommandedApp,
|
||||
PolicyService.Handlers.QuoteRequestHandler,
|
||||
PolicyService.Consumers.QuoteReceivedConsumer,
|
||||
PolicyService.Projectors.PolicyProjector,
|
||||
PolicyService.Consumers.PolicyIssuedConsumer,
|
||||
PolicyService.Handlers.SolicitationRequestHandler,
|
||||
PolicyServiceWeb.Telemetry,
|
||||
PolicyService.Repo,
|
||||
{DNSCluster, query: Application.get_env(:policy_service, :dns_cluster_query) || :ignore},
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
defmodule PolicyService.Router do
|
||||
use Commanded.Commands.Router
|
||||
alias PolicyService.Commands.Car
|
||||
alias PolicyService.Aggregates
|
||||
|
||||
# Route Car commands to Car Aggregate
|
||||
dispatch(
|
||||
[
|
||||
Car.SubmitCarPolicyApplication,
|
||||
Car.RecordCarProviderQuote,
|
||||
Car.AcceptCarQuoteAndSolicit,
|
||||
Car.RecordCarPolicyIssued
|
||||
PolicyService.Commands.CarPolicy.SubmitPolicyApplication,
|
||||
PolicyService.Commands.CarPolicy.RecordProviderQuote,
|
||||
PolicyService.Commands.CarPolicy.AcceptQuoteAndSolicit,
|
||||
PolicyService.Commands.CarPolicy.RecordPolicyIssued
|
||||
],
|
||||
to: PolicyService.Aggregates.CarPolicyApplication,
|
||||
identity: :application_id
|
||||
identity: :id
|
||||
)
|
||||
|
||||
# Route Fire commands to Fire Aggregate
|
||||
dispatch(
|
||||
[
|
||||
PolicyService.Commands.FirePolicy.SubmitPolicyApplication,
|
||||
PolicyService.Commands.FirePolicy.RecordProviderQuote,
|
||||
PolicyService.Commands.FirePolicy.AcceptQuoteAndSolicit,
|
||||
PolicyService.Commands.FirePolicy.RecordPolicyIssued
|
||||
],
|
||||
to: PolicyService.Aggregates.FirePolicyApplication,
|
||||
identity: :id
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
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
|
||||
8
lib/policy_service/commands/car_policy.ex
Normal file
8
lib/policy_service/commands/car_policy.ex
Normal file
@@ -0,0 +1,8 @@
|
||||
defmodule PolicyService.Commands.CarPolicy do
|
||||
alias PolicyService.Commands.Policy
|
||||
|
||||
defmodule SubmitPolicyApplication, do: use(Policy.SubmitPolicyApplication)
|
||||
defmodule RecordProviderQuote, do: use(Policy.RecordProviderQuote)
|
||||
defmodule AcceptQuoteAndSolicit, do: use(Policy.AcceptQuoteAndSolicit)
|
||||
defmodule RecordPolicyIssued, do: use(Policy.RecordPolicyIssued)
|
||||
end
|
||||
8
lib/policy_service/commands/fire_policy.ex
Normal file
8
lib/policy_service/commands/fire_policy.ex
Normal file
@@ -0,0 +1,8 @@
|
||||
defmodule PolicyService.Commands.FirePolicy do
|
||||
alias PolicyService.Commands.Policy
|
||||
|
||||
defmodule SubmitPolicyApplication, do: use(Policy.SubmitPolicyApplication)
|
||||
defmodule RecordProviderQuote, do: use(Policy.RecordProviderQuote)
|
||||
defmodule AcceptQuoteAndSolicit, do: use(Policy.AcceptQuoteAndSolicit)
|
||||
defmodule RecordPolicyIssued, do: use(Policy.RecordPolicyIssued)
|
||||
end
|
||||
65
lib/policy_service/commands/policy.ex
Normal file
65
lib/policy_service/commands/policy.ex
Normal file
@@ -0,0 +1,65 @@
|
||||
defmodule PolicyService.Commands.Policy do
|
||||
@moduledoc """
|
||||
Base templates for Policy commands.
|
||||
Use these macros to ensure all policy types share the same structure.
|
||||
"""
|
||||
|
||||
defmodule SubmitPolicyApplication do
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
defstruct [
|
||||
:id,
|
||||
:submitted_by,
|
||||
:applicant_info,
|
||||
:policy_details,
|
||||
:selected_providers
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule RecordProviderQuote do
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
defstruct [
|
||||
:id,
|
||||
:recorded_by,
|
||||
:provider_id,
|
||||
:quote_id,
|
||||
:premium,
|
||||
:coverage_details,
|
||||
:valid_until,
|
||||
:plans
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule AcceptQuoteAndSolicit do
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
defstruct [
|
||||
:id,
|
||||
:accepted_by,
|
||||
:quote_id,
|
||||
:plan_id,
|
||||
:solicitation_fields
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defmodule RecordPolicyIssued do
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
defstruct [
|
||||
:id,
|
||||
:policy_number,
|
||||
:effective_date,
|
||||
:expiry_date,
|
||||
:issued_at
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,16 +0,0 @@
|
||||
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
|
||||
@@ -1,6 +0,0 @@
|
||||
defmodule PolicyService.Common.ClientInfo do
|
||||
use ExConstructor
|
||||
|
||||
@derive Jason.Encoder
|
||||
defstruct [:first_name, :last_name, :birth_date, :gender, :email, :phone, :user_id]
|
||||
end
|
||||
73
lib/policy_service/consumers/policy_issued.ex
Normal file
73
lib/policy_service/consumers/policy_issued.ex
Normal file
@@ -0,0 +1,73 @@
|
||||
defmodule PolicyService.Consumers.PolicyIssuedConsumer do
|
||||
use GenServer
|
||||
require Logger
|
||||
|
||||
alias PolicyService.CommandedApp
|
||||
alias PolicyService.Commands.CarPolicy
|
||||
alias PolicyService.Aggregates.PolicyId
|
||||
|
||||
@exchange "carrier_inbox.events"
|
||||
@queue "policy_service.policy_issued"
|
||||
@routing_key "policy.issued"
|
||||
|
||||
def start_link(_opts), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
|
||||
def init(_) do
|
||||
{:ok, conn} = AMQP.Connection.open(amqp_url())
|
||||
{:ok, channel} = AMQP.Channel.open(conn)
|
||||
|
||||
AMQP.Exchange.topic(channel, @exchange, durable: true)
|
||||
AMQP.Queue.declare(channel, @queue, durable: true)
|
||||
AMQP.Queue.bind(channel, @queue, @exchange, routing_key: @routing_key)
|
||||
AMQP.Basic.qos(channel, prefetch_count: 10)
|
||||
{:ok, _tag} = AMQP.Basic.consume(channel, @queue)
|
||||
|
||||
{:ok, %{channel: channel}}
|
||||
end
|
||||
|
||||
def handle_info({:basic_consume_ok, _}, state), do: {:noreply, state}
|
||||
def handle_info({:basic_cancel, _}, state), do: {:stop, :normal, state}
|
||||
def handle_info({:basic_cancel_ok, _}, state), do: {:noreply, state}
|
||||
|
||||
def handle_info({:basic_deliver, payload, meta}, state) do
|
||||
case Jason.decode(payload) do
|
||||
{:ok, event} ->
|
||||
process(event, meta, state)
|
||||
|
||||
{:error, _} ->
|
||||
Logger.error("PolicyIssuedConsumer: failed to decode payload")
|
||||
AMQP.Basic.reject(state.channel, meta.delivery_tag, requeue: false)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
defp process(event, meta, state) do
|
||||
%{policy_type: policy_type} = PolicyId.parse!(event["id"])
|
||||
|
||||
command =
|
||||
case policy_type do
|
||||
"car" ->
|
||||
%CarPolicy.RecordPolicyIssued{
|
||||
id: event["id"],
|
||||
policy_number: event["policy_number"],
|
||||
effective_date: event["effective_date"],
|
||||
expiry_date: event["expiry_date"],
|
||||
issued_at: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
|
||||
case CommandedApp.dispatch(command) do
|
||||
:ok ->
|
||||
AMQP.Basic.ack(state.channel, meta.delivery_tag)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("PolicyIssuedConsumer: dispatch failed: #{inspect(reason)}")
|
||||
AMQP.Basic.reject(state.channel, meta.delivery_tag, requeue: true)
|
||||
end
|
||||
end
|
||||
|
||||
defp amqp_url do
|
||||
Application.get_env(:policy_service, :amqp_url, "amqp://guest:guest@localhost:5672")
|
||||
end
|
||||
end
|
||||
120
lib/policy_service/consumers/quote_received.ex
Normal file
120
lib/policy_service/consumers/quote_received.ex
Normal file
@@ -0,0 +1,120 @@
|
||||
defmodule PolicyService.Consumers.QuoteReceivedConsumer do
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
alias PolicyService.CommandedApp
|
||||
alias PolicyService.Commands.CarPolicy
|
||||
alias PolicyService.Aggregates.PolicyId
|
||||
|
||||
@exchange "carrier_inbox.events"
|
||||
@queue "policy_service.quote_received"
|
||||
@routing_key "quote.received"
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
def init(_opts) do
|
||||
amqp_url = Application.fetch_env!(:policy_service, :amqp_url)
|
||||
|
||||
{:ok, conn} = AMQP.Connection.open(amqp_url)
|
||||
{:ok, channel} = AMQP.Channel.open(conn)
|
||||
|
||||
AMQP.Exchange.declare(channel, @exchange, :topic, durable: true)
|
||||
|
||||
AMQP.Queue.declare(channel, @queue, durable: true)
|
||||
AMQP.Queue.bind(channel, @queue, @exchange, routing_key: @routing_key)
|
||||
|
||||
AMQP.Basic.consume(channel, @queue, nil, no_ack: false)
|
||||
|
||||
Logger.info("QuoteReceivedConsumer started, listening on #{@queue}")
|
||||
|
||||
{:ok, %{conn: conn, channel: channel}}
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AMQP callbacks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def handle_info({:basic_consume_ok, _}, state), do: {:noreply, state}
|
||||
def handle_info({:basic_cancel, _}, state), do: {:stop, :normal, state}
|
||||
def handle_info({:basic_cancel_ok, _}, state), do: {:noreply, state}
|
||||
|
||||
def handle_info({:basic_deliver, payload, %{delivery_tag: tag}}, state) do
|
||||
case process(payload) do
|
||||
:ok ->
|
||||
AMQP.Basic.ack(state.channel, tag)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to process quote.received: #{inspect(reason)}")
|
||||
AMQP.Basic.nack(state.channel, tag, requeue: false)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Processing
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp process(payload) do
|
||||
with {:ok, event} <- Jason.decode(payload),
|
||||
{:ok, cmd} <- build_command(event),
|
||||
:ok <- CommandedApp.dispatch(cmd, consistency: :strong) do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp build_command(event) do
|
||||
case event["policy_type"] do
|
||||
"car" -> build_car_command(event)
|
||||
type -> {:error, {:unsupported_policy_type, type}}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_car_command(event) do
|
||||
%{policy_type: policy_type} = PolicyId.parse!(event["id"])
|
||||
|
||||
case policy_type do
|
||||
"car" ->
|
||||
cmd = %CarPolicy.RecordProviderQuote{
|
||||
id: PolicyId.parse!(event["id"]),
|
||||
recorded_by: event["entered_by"],
|
||||
provider_id: event["provider_id"],
|
||||
quote_id: event["quote_id"],
|
||||
valid_until: parse_date(event["valid_until"]),
|
||||
plans: parse_plans(event["plans"])
|
||||
}
|
||||
|
||||
{:ok, cmd}
|
||||
end
|
||||
rescue
|
||||
e -> {:error, e}
|
||||
end
|
||||
|
||||
defp parse_plans(nil), do: []
|
||||
|
||||
defp parse_plans(plans) when is_list(plans) do
|
||||
Enum.map(plans, fn p ->
|
||||
%{
|
||||
plan_id: p["plan_id"],
|
||||
name: p["name"],
|
||||
premium: p["premium"],
|
||||
coverage_details: p["coverage_details"],
|
||||
deductible: p["deductible"],
|
||||
coverage_limit: p["coverage_limit"]
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp parse_date(nil), do: nil
|
||||
defp parse_date(%Date{} = d), do: d
|
||||
|
||||
defp parse_date(s) when is_binary(s) do
|
||||
case Date.from_iso8601(s) do
|
||||
{:ok, d} -> d
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,69 +0,0 @@
|
||||
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
|
||||
80
lib/policy_service/events/policy.ex
Normal file
80
lib/policy_service/events/policy.ex
Normal file
@@ -0,0 +1,80 @@
|
||||
defmodule PolicyService.Events.Policy do
|
||||
defmodule PolicyApplicationSubmitted do
|
||||
@derive Jason.Encoder
|
||||
defstruct [
|
||||
:id,
|
||||
:submitted_by,
|
||||
:applicant_info,
|
||||
:policy_details,
|
||||
:selected_providers,
|
||||
:submitted_at
|
||||
]
|
||||
end
|
||||
|
||||
defmodule QuoteRequestSent do
|
||||
@derive Jason.Encoder
|
||||
defstruct [
|
||||
:id,
|
||||
:provider_id,
|
||||
:provider_email,
|
||||
:applicant_info,
|
||||
:policy_details,
|
||||
:requested_at
|
||||
]
|
||||
end
|
||||
|
||||
defmodule ProviderQuoteReceived do
|
||||
@derive Jason.Encoder
|
||||
defstruct [
|
||||
:id,
|
||||
:recorded_by,
|
||||
:provider_id,
|
||||
:quote_id,
|
||||
:premium,
|
||||
:coverage_details,
|
||||
:valid_until,
|
||||
:plans,
|
||||
:received_at
|
||||
]
|
||||
end
|
||||
|
||||
defmodule AllQuotesReceived do
|
||||
@derive Jason.Encoder
|
||||
defstruct [:id, :org_id, :quote_count]
|
||||
end
|
||||
|
||||
defmodule QuoteAccepted do
|
||||
@derive Jason.Encoder
|
||||
defstruct [
|
||||
:id,
|
||||
:accepted_by,
|
||||
:quote,
|
||||
:plan,
|
||||
:provider,
|
||||
:accepted_at
|
||||
]
|
||||
end
|
||||
|
||||
defmodule SolicitationSent do
|
||||
@derive Jason.Encoder
|
||||
defstruct [
|
||||
:id,
|
||||
:solicitation_id,
|
||||
:provider_id,
|
||||
:template_id,
|
||||
:s3_key,
|
||||
:sent_at
|
||||
]
|
||||
end
|
||||
|
||||
defmodule PolicyIssued do
|
||||
@derive Jason.Encoder
|
||||
defstruct [
|
||||
:id,
|
||||
:policy_number,
|
||||
:effective_date,
|
||||
:expiry_date,
|
||||
:issued_at
|
||||
]
|
||||
end
|
||||
end
|
||||
@@ -3,19 +3,9 @@ defmodule PolicyService.Handlers.QuoteRequestHandler do
|
||||
application: PolicyService.CommandedApp,
|
||||
name: __MODULE__
|
||||
|
||||
alias PolicyService.Events.Car.CarQuoteRequestSent
|
||||
# alias PolicyService.Events.Life.LifeQuoteRequestSent
|
||||
# alias PolicyService.Events.Fire.FireQuoteRequestSent
|
||||
alias PolicyService.Events.Policy.QuoteRequestSent
|
||||
|
||||
def handle(%CarQuoteRequestSent{} = e, _metadata) do
|
||||
PolicyService.MessageBus.publish("carquote.requested", e)
|
||||
def handle(%QuoteRequestSent{} = e, _metadata) do
|
||||
PolicyService.MessageBus.publish("quote.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
|
||||
|
||||
15
lib/policy_service/handlers/solicitation_request_handler.ex
Normal file
15
lib/policy_service/handlers/solicitation_request_handler.ex
Normal file
@@ -0,0 +1,15 @@
|
||||
defmodule PolicyService.Handlers.SolicitationRequestHandler do
|
||||
use Commanded.Event.Handler,
|
||||
application: PolicyService.CommandedApp,
|
||||
name: "SolicitationRequestHandler"
|
||||
|
||||
require Logger
|
||||
|
||||
alias PolicyService.Events.Policy.QuoteAccepted
|
||||
alias PolicyService.MessageBus
|
||||
|
||||
def handle(%QuoteAccepted{} = event, _metadata) do
|
||||
MessageBus.publish("quote.accepted", event)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -7,7 +7,6 @@ defmodule PolicyService.MessageBus do
|
||||
:ok =
|
||||
AMQP.Basic.publish(channel(), "policy_service.events", routing_key, payload,
|
||||
content_type: "application/json",
|
||||
# survives RabbitMQ restart
|
||||
persistent: true
|
||||
)
|
||||
end
|
||||
|
||||
17
lib/policy_service/policy/filters.ex
Normal file
17
lib/policy_service/policy/filters.ex
Normal file
@@ -0,0 +1,17 @@
|
||||
defmodule PolicyService.Filters.PolicyApplicationFilters do
|
||||
import Ecto.Query
|
||||
|
||||
def search(query, %Flop.Filter{value: value}, _opts) do
|
||||
term = "%#{value}%"
|
||||
|
||||
where(
|
||||
query,
|
||||
[p],
|
||||
fragment("?->>'name' ilike ?", p.applicant_info, ^term) or
|
||||
fragment("?->>'company_name' ilike ?", p.applicant_info, ^term) or
|
||||
fragment("?->>'document_id' ilike ?", p.applicant_info, ^term) or
|
||||
fragment("?->>'ruc' ilike ?", p.applicant_info, ^term) or
|
||||
ilike(p.policy_number, ^term)
|
||||
)
|
||||
end
|
||||
end
|
||||
21
lib/policy_service/policy/queries.ex
Normal file
21
lib/policy_service/policy/queries.ex
Normal file
@@ -0,0 +1,21 @@
|
||||
defmodule PolicyService.Queries.PolicyQueries do
|
||||
import Ecto.Query
|
||||
|
||||
alias PolicyService.Repo
|
||||
alias PolicyService.Projections.PolicyApplication
|
||||
|
||||
def list_by_org(org_id, params \\ %{}) do
|
||||
base = from(p in PolicyApplication, where: p.org_id == ^org_id)
|
||||
Flop.validate_and_run(base, params, for: PolicyApplication)
|
||||
end
|
||||
|
||||
def get_by_application_id(org_id, application_id) do
|
||||
case Repo.get_by(PolicyApplication,
|
||||
application_id: application_id,
|
||||
org_id: org_id
|
||||
) do
|
||||
nil -> {:error, :not_found}
|
||||
p -> {:ok, p}
|
||||
end
|
||||
end
|
||||
end
|
||||
85
lib/policy_service/projections/policy.ex
Normal file
85
lib/policy_service/projections/policy.ex
Normal file
@@ -0,0 +1,85 @@
|
||||
defmodule PolicyService.Projections.PolicyApplication do
|
||||
use Ecto.Schema
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
:id,
|
||||
:application_id,
|
||||
:org_id,
|
||||
:submitted_by,
|
||||
:policy_type,
|
||||
:applicant_info,
|
||||
:policy_details,
|
||||
:selected_providers,
|
||||
:quotes,
|
||||
:accepted_quote_id,
|
||||
:accepted_plan_id,
|
||||
:accepted_provider_id,
|
||||
:accepted_by,
|
||||
:accepted_at,
|
||||
:solicitation_id,
|
||||
:solicitation_s3_key,
|
||||
:policy_number,
|
||||
:premium,
|
||||
:effective_date,
|
||||
:expiry_date,
|
||||
:status,
|
||||
:submitted_at,
|
||||
:solicitation_sent_at,
|
||||
:issued_at,
|
||||
:inserted_at,
|
||||
:updated_at
|
||||
]}
|
||||
|
||||
@derive {
|
||||
Flop.Schema,
|
||||
filterable: [:org_id, :policy_type, :status, :search],
|
||||
sortable: [:submitted_at, :policy_type, :status],
|
||||
default_limit: 20,
|
||||
max_limit: 100,
|
||||
custom_fields: [
|
||||
search: [
|
||||
filter: {PolicyService.Projections.PolicyApplicationFilters, :search, []},
|
||||
ecto_type: :string,
|
||||
operators: [:==]
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@primary_key {:id, :string, autogenerate: false}
|
||||
@timestamps_opts [type: :utc_datetime_usec]
|
||||
|
||||
schema "policy_applications" do
|
||||
field :application_id, :string
|
||||
field :org_id, :string
|
||||
field :submitted_by, :string
|
||||
field :policy_type, :string
|
||||
|
||||
field :applicant_info, :map
|
||||
field :policy_details, :map
|
||||
|
||||
field :selected_providers, {:array, :string}, default: []
|
||||
field :quotes, :map, default: %{}
|
||||
|
||||
field :accepted_quote_id, :string
|
||||
field :accepted_plan_id, :string
|
||||
field :accepted_provider_id, :string
|
||||
field :accepted_by, :string
|
||||
field :accepted_at, :utc_datetime_usec
|
||||
|
||||
field :solicitation_id, :string
|
||||
field :solicitation_s3_key, :string
|
||||
|
||||
field :policy_number, :string
|
||||
field :premium, :decimal
|
||||
field :effective_date, :date
|
||||
field :expiry_date, :date
|
||||
|
||||
field :status, :string
|
||||
field :submitted_at, :utc_datetime_usec
|
||||
field :solicitation_sent_at, :utc_datetime_usec
|
||||
field :issued_at, :utc_datetime_usec
|
||||
|
||||
timestamps()
|
||||
end
|
||||
end
|
||||
144
lib/policy_service/projectors/policy_projector.ex
Normal file
144
lib/policy_service/projectors/policy_projector.ex
Normal file
@@ -0,0 +1,144 @@
|
||||
defmodule PolicyService.Projectors.PolicyProjector do
|
||||
use Commanded.Projections.Ecto,
|
||||
application: PolicyService.CommandedApp,
|
||||
repo: PolicyService.Repo,
|
||||
name: "PolicyApplicationProjection",
|
||||
consistency: :strong
|
||||
|
||||
alias PolicyService.Events.Policy.{
|
||||
PolicyApplicationSubmitted,
|
||||
ProviderQuoteReceived,
|
||||
AllQuotesReceived,
|
||||
QuoteAccepted,
|
||||
SolicitationSent,
|
||||
PolicyIssued
|
||||
}
|
||||
|
||||
alias PolicyService.Projections.PolicyApplication
|
||||
alias PolicyService.Aggregates.PolicyId
|
||||
import Ecto.Query
|
||||
|
||||
project(%PolicyApplicationSubmitted{} = e, _meta, fn multi ->
|
||||
%{policy_type: policy_type, application_id: application_id, org_id: org_id} = e.id
|
||||
|
||||
Ecto.Multi.insert(multi, :policy_application, %PolicyApplication{
|
||||
id: to_string(PolicyId.new(org_id, policy_type, application_id)),
|
||||
application_id: application_id,
|
||||
org_id: org_id,
|
||||
submitted_by: e.submitted_by,
|
||||
policy_type: policy_type,
|
||||
applicant_info: atomize(e.applicant_info),
|
||||
policy_details: atomize(e.policy_details),
|
||||
selected_providers: Enum.map(e.selected_providers, & &1["provider_id"]),
|
||||
quotes: %{},
|
||||
status: "quote_requested",
|
||||
submitted_at: parse_datetime(e.submitted_at)
|
||||
})
|
||||
end)
|
||||
|
||||
project(%ProviderQuoteReceived{} = e, _meta, fn multi ->
|
||||
multi
|
||||
|> Ecto.Multi.run(:fetch, fn repo, _ ->
|
||||
{:ok, repo.get!(PolicyApplication, to_string(e.id))}
|
||||
end)
|
||||
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
|
||||
quote_data = %{
|
||||
"quote_id" => e.quote_id,
|
||||
"provider_id" => e.provider_id,
|
||||
"valid_until" => e.valid_until,
|
||||
"received_at" => parse_datetime(e.received_at),
|
||||
"plans" => e.plans || []
|
||||
}
|
||||
|
||||
Ecto.Changeset.change(p, quotes: Map.put(p.quotes, e.provider_id, quote_data))
|
||||
end)
|
||||
end)
|
||||
|
||||
project(%AllQuotesReceived{} = e, _meta, fn multi ->
|
||||
multi
|
||||
|> Ecto.Multi.run(:fetch, fn repo, _ ->
|
||||
{:ok, repo.get!(PolicyApplication, to_string(e.id))}
|
||||
end)
|
||||
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
|
||||
Ecto.Changeset.change(p, status: "quotes_received")
|
||||
end)
|
||||
end)
|
||||
|
||||
project(%QuoteAccepted{} = e, _meta, fn multi ->
|
||||
multi
|
||||
|> Ecto.Multi.run(:fetch, fn repo, _ ->
|
||||
{:ok, repo.get!(PolicyApplication, to_string(e.id))}
|
||||
end)
|
||||
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
|
||||
Ecto.Changeset.change(p,
|
||||
accepted_quote_id: e.quote.quote_id,
|
||||
accepted_plan_id: e.plan.plan_id,
|
||||
accepted_provider_id: e.provider.id,
|
||||
accepted_at: parse_datetime(e.accepted_at),
|
||||
status: "solicitation_sent"
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
project(%SolicitationSent{} = e, _meta, fn multi ->
|
||||
multi
|
||||
|> Ecto.Multi.run(:fetch, fn repo, _ ->
|
||||
{:ok, repo.get!(PolicyApplication, to_string(e.id))}
|
||||
end)
|
||||
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
|
||||
Ecto.Changeset.change(p,
|
||||
solicitation_id: e.solicitation_id,
|
||||
solicitation_s3_key: e.s3_key,
|
||||
solicitation_sent_at: parse_datetime(e.sent_at)
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
project(%PolicyIssued{} = e, _meta, fn multi ->
|
||||
multi
|
||||
|> Ecto.Multi.run(:fetch, fn repo, _ ->
|
||||
{:ok, repo.get!(PolicyApplication, to_string(e.id))}
|
||||
end)
|
||||
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
|
||||
Ecto.Changeset.change(p,
|
||||
policy_number: e.policy_number,
|
||||
effective_date: parse_date(e.effective_date),
|
||||
expiry_date: parse_date(e.expiry_date),
|
||||
issued_at: parse_datetime(e.issued_at),
|
||||
status: "issued"
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp atomize(nil), do: nil
|
||||
|
||||
defp atomize(map) when is_map(map) do
|
||||
Map.new(map, fn {k, v} ->
|
||||
{if(is_atom(k), do: Atom.to_string(k), else: k), v}
|
||||
end)
|
||||
end
|
||||
|
||||
defp parse_datetime(nil), do: nil
|
||||
defp parse_datetime(%DateTime{} = dt), do: dt
|
||||
|
||||
defp parse_datetime(str) when is_binary(str) do
|
||||
case DateTime.from_iso8601(str) do
|
||||
{:ok, dt, _} -> dt
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_date(nil), do: nil
|
||||
defp parse_date(%Date{} = d), do: d
|
||||
|
||||
defp parse_date(str) when is_binary(str) do
|
||||
case Date.from_iso8601(str) do
|
||||
{:ok, d} -> d
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,85 +0,0 @@
|
||||
# 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
|
||||
378
lib/policy_service_web/controllers/policy_controller.ex
Normal file
378
lib/policy_service_web/controllers/policy_controller.ex
Normal file
@@ -0,0 +1,378 @@
|
||||
defmodule PolicyServiceWeb.PolicyController do
|
||||
use PolicyServiceWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
alias PolicyService.CommandedApp
|
||||
alias PolicyService.Queries.PolicyQueries
|
||||
alias PolicyService.Aggregates.PolicyId
|
||||
|
||||
alias PolicyService.Commands.CarPolicy
|
||||
|
||||
alias PolicyServiceWeb.Schemas.Policy, as: S
|
||||
|
||||
tags(["Policies"])
|
||||
security([%{"bearerAuth" => []}])
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/policies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
operation(:index,
|
||||
summary: "List policies",
|
||||
parameters: [
|
||||
"page[number]": [in: :query, type: :integer, required: false],
|
||||
"page[size]": [in: :query, type: :integer, required: false],
|
||||
"filters[0][field]": [in: :query, type: :string, required: false],
|
||||
"filters[0][op]": [in: :query, type: :string, required: false],
|
||||
"filters[0][value]": [in: :query, type: :string, required: false],
|
||||
"filters[1][field]": [in: :query, type: :string, required: false],
|
||||
"filters[1][op]": [in: :query, type: :string, required: false],
|
||||
"filters[1][value]": [in: :query, type: :string, required: false],
|
||||
"order_by[]": [in: :query, type: :string, required: false]
|
||||
],
|
||||
responses: [
|
||||
ok: {"Policy list", "application/json", S.PolicyListResponse},
|
||||
bad_request: {"Invalid params", "application/json", S.ErrorResponse}
|
||||
]
|
||||
)
|
||||
|
||||
def index(conn, params) do
|
||||
org_id = conn.assigns[:org_id] || "test"
|
||||
|
||||
case PolicyQueries.list_by_org(org_id, params) do
|
||||
{:ok, {policies, meta}} ->
|
||||
conn
|
||||
|> put_status(:ok)
|
||||
|> json(%{
|
||||
data: Enum.map(policies, &policy_summary/1),
|
||||
meta: meta_json(meta)
|
||||
})
|
||||
|
||||
{:error, _} ->
|
||||
conn |> put_status(:bad_request) |> json(%{error: "invalid parameters"})
|
||||
end
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/policies/:application_id
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
operation(:show,
|
||||
summary: "Get policy detail",
|
||||
parameters: [
|
||||
application_id: [in: :path, type: :string, required: true]
|
||||
],
|
||||
responses: [
|
||||
ok: {"Policy detail", "application/json", S.PolicyDetailResponse},
|
||||
not_found: {"Not found", "application/json", S.ErrorResponse}
|
||||
]
|
||||
)
|
||||
|
||||
def show(conn, %{"application_id" => application_id}) do
|
||||
org_id = conn.assigns[:org_id] || "test"
|
||||
|
||||
case PolicyQueries.get_by_application_id(org_id, application_id) do
|
||||
{:ok, policy} ->
|
||||
conn |> put_status(:ok) |> json(%{data: policy_detail(policy)})
|
||||
|
||||
{:error, :not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "policy not found"})
|
||||
end
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /api/policies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
operation(:create,
|
||||
summary: "Submit a policy quote request",
|
||||
request_body: {"Quote request", "application/json", S.CreatePolicyRequest, required: true},
|
||||
responses: [
|
||||
created: {"Submitted", "application/json", S.QuoteResponse},
|
||||
unprocessable_entity: {"Validation error", "application/json", S.ErrorResponse}
|
||||
]
|
||||
)
|
||||
|
||||
def create(conn, params) do
|
||||
application_id = Ecto.UUID.generate()
|
||||
org_id = conn.assigns[:org_id] || "test"
|
||||
submitted_by = conn.assigns[:user_id] || "test"
|
||||
|
||||
with {:ok, policy_type} <- parse_policy_type(params["policy_type"]),
|
||||
{:ok, applicant_info} <- parse_applicant_info(params["applicant_info"]),
|
||||
{:ok, policy_details} <- parse_policy_details(policy_type, params["policy_details"]),
|
||||
{:ok, providers} <- parse_providers(params["selected_providers"]) do
|
||||
command =
|
||||
case policy_type do
|
||||
"car" ->
|
||||
%CarPolicy.SubmitPolicyApplication{
|
||||
id: PolicyId.new(org_id, policy_type, application_id),
|
||||
submitted_by: submitted_by,
|
||||
applicant_info: applicant_info,
|
||||
policy_details: policy_details,
|
||||
selected_providers: providers
|
||||
}
|
||||
end
|
||||
|
||||
case CommandedApp.dispatch(command, consistency: :strong) do
|
||||
:ok ->
|
||||
conn
|
||||
|> put_status(:created)
|
||||
|> json(%{application_id: application_id, status: "awaiting_quotes"})
|
||||
|
||||
{:error, reason} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: inspect(reason)})
|
||||
end
|
||||
else
|
||||
{:error, reason} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: inspect(reason)})
|
||||
end
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# POST /api/policies/:application_id/accept
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
operation(:accept,
|
||||
summary: "Accept a quote plan and trigger solicitation",
|
||||
parameters: [
|
||||
application_id: [in: :path, type: :string, required: true]
|
||||
],
|
||||
request_body: {"Accept quote", "application/json", S.AcceptQuoteRequest, required: true},
|
||||
responses: [
|
||||
ok: {"Accepted", "application/json", S.PolicyDetailResponse},
|
||||
not_found: {"Not found", "application/json", S.ErrorResponse},
|
||||
unprocessable_entity: {"Error", "application/json", S.ErrorResponse}
|
||||
]
|
||||
)
|
||||
|
||||
def accept(conn, %{"application_id" => application_id} = params) do
|
||||
org_id = conn.assigns[:org_id] || "test"
|
||||
|
||||
with {:ok, policy} <- PolicyQueries.get_by_application_id(org_id, application_id) do
|
||||
command =
|
||||
case policy.policy_type do
|
||||
"car" ->
|
||||
%CarPolicy.AcceptQuoteAndSolicit{
|
||||
id: PolicyId.new(org_id, policy.policy_type, application_id),
|
||||
quote_id: params["quote_id"],
|
||||
plan_id: params["plan_id"],
|
||||
solicitation_fields: params["solicitation_fields"] || %{}
|
||||
}
|
||||
end
|
||||
|
||||
case CommandedApp.dispatch(command, consistency: :strong) do
|
||||
:ok ->
|
||||
{:ok, updated} = PolicyQueries.get_by_application_id(org_id, application_id)
|
||||
conn |> put_status(:ok) |> json(%{data: policy_detail(updated)})
|
||||
|
||||
{:error, :quote_not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "quote not found"})
|
||||
|
||||
{:error, :plan_not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "plan not found"})
|
||||
|
||||
{:error, reason} ->
|
||||
conn |> put_status(:unprocessable_entity) |> json(%{error: inspect(reason)})
|
||||
end
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "policy not found"})
|
||||
end
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /api/policies/:application_id/solicitation-url
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
operation(:solicitation_url,
|
||||
summary: "Get fresh presigned download URL for solicitation PDF",
|
||||
parameters: [
|
||||
application_id: [in: :path, type: :string, required: true],
|
||||
version: [in: :query, type: :integer, required: false]
|
||||
],
|
||||
responses: [
|
||||
ok: {"Presigned URL", "application/json", S.SolicitationUrlResponse},
|
||||
not_found: {"Not found", "application/json", S.ErrorResponse}
|
||||
]
|
||||
)
|
||||
|
||||
def solicitation_url(conn, %{"application_id" => application_id} = params) do
|
||||
org_id = conn.assigns[:org_id] || "test"
|
||||
version = String.to_integer(params["version"] || "1")
|
||||
|
||||
case PolicyQueries.get_by_application_id(org_id, application_id) do
|
||||
{:error, :not_found} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "policy not found"})
|
||||
|
||||
{:ok, %{solicitation_id: nil}} ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "no solicitation yet"})
|
||||
|
||||
{:ok, policy} ->
|
||||
url =
|
||||
"#{solicitation_service_url()}/api/solicitations/#{policy.solicitation_id}/download-url"
|
||||
|
||||
case Req.get(url,
|
||||
params: [org_id: org_id, application_id: application_id, version: version]
|
||||
) do
|
||||
{:ok, %{status: 200, body: body}} ->
|
||||
conn |> put_status(:ok) |> json(body)
|
||||
|
||||
{:ok, %{status: status, body: body}} ->
|
||||
conn
|
||||
|> put_status(:bad_gateway)
|
||||
|> json(%{error: "solicitation service returned #{status}: #{inspect(body)}"})
|
||||
|
||||
{:error, reason} ->
|
||||
conn |> put_status(:bad_gateway) |> json(%{error: inspect(reason)})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Serializers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp policy_summary(p) do
|
||||
%{
|
||||
application_id: p.application_id,
|
||||
policy_type: p.policy_type,
|
||||
status: p.status,
|
||||
applicant_info: p.applicant_info,
|
||||
policy_details: p.policy_details,
|
||||
policy_number: p.policy_number,
|
||||
submitted_at: p.submitted_at
|
||||
}
|
||||
end
|
||||
|
||||
defp policy_detail(p) do
|
||||
%{
|
||||
application_id: p.application_id,
|
||||
org_id: p.org_id,
|
||||
submitted_by: p.submitted_by,
|
||||
policy_type: p.policy_type,
|
||||
status: p.status,
|
||||
applicant_info: p.applicant_info,
|
||||
policy_details: p.policy_details,
|
||||
selected_providers: p.selected_providers,
|
||||
quotes: p.quotes,
|
||||
accepted_quote_id: p.accepted_quote_id,
|
||||
accepted_plan_id: p.accepted_plan_id,
|
||||
accepted_provider_id: p.accepted_provider_id,
|
||||
accepted_at: p.accepted_at,
|
||||
solicitation_id: p.solicitation_id,
|
||||
solicitation_s3_key: p.solicitation_s3_key,
|
||||
policy_number: p.policy_number,
|
||||
premium: p.premium,
|
||||
effective_date: p.effective_date,
|
||||
expiry_date: p.expiry_date,
|
||||
submitted_at: p.submitted_at,
|
||||
solicitation_sent_at: p.solicitation_sent_at,
|
||||
issued_at: p.issued_at
|
||||
}
|
||||
end
|
||||
|
||||
defp meta_json(meta) do
|
||||
%{
|
||||
total_count: meta.total_count,
|
||||
total_pages: meta.total_pages,
|
||||
current_page: meta.current_page,
|
||||
page_size: meta.page_size,
|
||||
has_next: meta.has_next_page?,
|
||||
has_prev: meta.has_previous_page?
|
||||
}
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parse helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp parse_policy_type(type) when type in ["car", "life", "fire"], do: {:ok, type}
|
||||
defp parse_policy_type(_), do: {:error, :invalid_policy_type}
|
||||
|
||||
# individual — has document_id
|
||||
defp parse_applicant_info(%{"document_id" => doc} = info)
|
||||
when is_binary(doc) and byte_size(doc) > 0 do
|
||||
case info["date_of_birth"] do
|
||||
nil ->
|
||||
{:error, :missing_date_of_birth}
|
||||
|
||||
dob ->
|
||||
{:ok,
|
||||
%{
|
||||
"name" => info["name"],
|
||||
"date_of_birth" => dob,
|
||||
"document_id" => doc
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
# corporate — has ruc
|
||||
defp parse_applicant_info(%{"ruc" => ruc} = info)
|
||||
when is_binary(ruc) and byte_size(ruc) > 0 do
|
||||
{:ok,
|
||||
%{
|
||||
"company_name" => info["company_name"],
|
||||
"ruc" => ruc,
|
||||
"legal_rep_name" => info["legal_rep_name"],
|
||||
"legal_rep_document" => info["legal_rep_document"]
|
||||
}}
|
||||
end
|
||||
|
||||
defp parse_applicant_info(_), do: {:error, :invalid_applicant_info}
|
||||
|
||||
# car details
|
||||
defp parse_policy_details("car", nil), do: {:error, :missing_policy_details}
|
||||
|
||||
defp parse_policy_details("car", d) do
|
||||
{:ok,
|
||||
%{
|
||||
"plate" => d["plate"],
|
||||
"make" => d["make"],
|
||||
"model" => d["model"],
|
||||
"year" => d["year"],
|
||||
"car_value" => d["car_value"],
|
||||
"use_type" => d["use_type"],
|
||||
"car_type" => d["car_type"],
|
||||
"chassis_number" => d["chassis_number"],
|
||||
"engine_number" => d["engine_number"]
|
||||
}}
|
||||
end
|
||||
|
||||
# life details
|
||||
defp parse_policy_details("life", nil), do: {:error, :missing_policy_details}
|
||||
|
||||
defp parse_policy_details("life", d) do
|
||||
{:ok,
|
||||
%{
|
||||
"coverage_amount" => d["coverage_amount"],
|
||||
"beneficiary" => d["beneficiary"]
|
||||
}}
|
||||
end
|
||||
|
||||
# fire details
|
||||
defp parse_policy_details("fire", nil), do: {:error, :missing_policy_details}
|
||||
|
||||
defp parse_policy_details("fire", d) do
|
||||
{:ok,
|
||||
%{
|
||||
"property_address" => d["property_address"],
|
||||
"property_value" => d["property_value"]
|
||||
}}
|
||||
end
|
||||
|
||||
defp parse_policy_details(_, _), do: {:error, :invalid_policy_details}
|
||||
|
||||
defp parse_providers(nil), do: {:error, :missing_providers}
|
||||
defp parse_providers([]), do: {:error, :no_providers_selected}
|
||||
|
||||
defp parse_providers(list) when is_list(list) do
|
||||
{:ok, Enum.map(list, fn p -> %{provider_id: p["provider_id"], email: p["email"]} end)}
|
||||
end
|
||||
|
||||
defp parse_providers(_), do: {:error, :invalid_providers}
|
||||
|
||||
defp solicitation_service_url do
|
||||
Application.get_env(:policy_service, :solicitation_service_url, "http://localhost:8081")
|
||||
end
|
||||
end
|
||||
@@ -42,6 +42,7 @@ defmodule PolicyServiceWeb.Endpoint do
|
||||
pass: ["*/*"],
|
||||
json_decoder: Phoenix.json_library()
|
||||
|
||||
plug CORSPlug, origin: ["http://localhost:3000"]
|
||||
plug Plug.MethodOverride
|
||||
plug Plug.Head
|
||||
plug Plug.Session, @session_options
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
defmodule PolicyServiceWeb.Router do
|
||||
use PolicyServiceWeb, :router
|
||||
|
||||
alias PolicyServiceWeb.PolicyController
|
||||
|
||||
pipeline :api do
|
||||
plug OpenApiSpex.Plug.PutApiSpec, module: PolicyServiceWeb.ApiSpec
|
||||
end
|
||||
@@ -11,13 +13,14 @@ defmodule PolicyServiceWeb.Router do
|
||||
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
|
||||
|
||||
scope "/v1" do
|
||||
scope "/car-policies" do
|
||||
post "/quotes", PolicyServiceWeb.CarPolicyController, :request_quote
|
||||
end
|
||||
get "/policies", PolicyController, :index
|
||||
get "/policies/:application_id", PolicyController, :show
|
||||
post "/policies", PolicyController, :create
|
||||
post "/policies/:application_id/accept", PolicyController, :accept
|
||||
get "/policies/:application_id/solicitation-url", PolicyController, :solicitation_url
|
||||
end
|
||||
end
|
||||
|
||||
# Swagger UI — only in dev
|
||||
if Mix.env() == :dev do
|
||||
scope "/swaggerui" do
|
||||
get "/", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
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
|
||||
368
lib/policy_service_web/schemas/policy.ex
Normal file
368
lib/policy_service_web/schemas/policy.ex
Normal file
@@ -0,0 +1,368 @@
|
||||
defmodule PolicyServiceWeb.Schemas.Policy do
|
||||
alias OpenApiSpex.Schema
|
||||
|
||||
defmodule PaginationMeta do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "PaginationMeta",
|
||||
type: :object,
|
||||
properties: %{
|
||||
total_count: %Schema{type: :integer},
|
||||
total_pages: %Schema{type: :integer},
|
||||
current_page: %Schema{type: :integer},
|
||||
page_size: %Schema{type: :integer},
|
||||
has_next: %Schema{type: :boolean},
|
||||
has_prev: %Schema{type: :boolean}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Applicant — discriminated by presence of keys
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defmodule ApplicantIndividual do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ApplicantIndividual",
|
||||
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: "8-123-456"}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule ApplicantCorporate do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ApplicantCorporate",
|
||||
type: :object,
|
||||
required: [:company_name, :ruc, :legal_rep_name, :legal_rep_document],
|
||||
properties: %{
|
||||
company_name: %Schema{type: :string, example: "Empresa ABC S.A."},
|
||||
ruc: %Schema{type: :string, example: "123456-1-123456"},
|
||||
legal_rep_name: %Schema{type: :string, example: "María García"},
|
||||
legal_rep_document: %Schema{type: :string, example: "8-456-789"}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule ApplicantInfo do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ApplicantInfo",
|
||||
oneOf: [ApplicantIndividual, ApplicantCorporate]
|
||||
})
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Policy details — one per policy type
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defmodule CarPolicyDetails do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "CarPolicyDetails",
|
||||
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"]},
|
||||
car_type: %Schema{
|
||||
type: :string,
|
||||
enum: [
|
||||
"sedan",
|
||||
"suv",
|
||||
"hatchback",
|
||||
"coupe",
|
||||
"convertible",
|
||||
"pickup",
|
||||
"van",
|
||||
"minivan",
|
||||
"truck"
|
||||
]
|
||||
},
|
||||
chassis_number: %Schema{type: :string, example: "9BWZZZ377VT004251"},
|
||||
engine_number: %Schema{type: :string, example: "1NZ-FE-1234567"}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule LifePolicyDetails do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "LifePolicyDetails",
|
||||
type: :object,
|
||||
required: [:coverage_amount, :beneficiary],
|
||||
properties: %{
|
||||
coverage_amount: %Schema{type: :number, example: 100_000},
|
||||
beneficiary: %Schema{type: :string, example: "María Pérez"}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule FirePolicyDetails do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "FirePolicyDetails",
|
||||
type: :object,
|
||||
required: [:property_address, :property_value],
|
||||
properties: %{
|
||||
property_address: %Schema{type: :string, example: "Calle 50, Panama City"},
|
||||
property_value: %Schema{type: :number, example: 250_000}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule PolicyDetails do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "PolicyDetails",
|
||||
oneOf: [CarPolicyDetails, LifePolicyDetails, FirePolicyDetails]
|
||||
})
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defmodule SelectedProvider do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "SelectedProvider",
|
||||
type: :object,
|
||||
required: [:provider_id, :email],
|
||||
properties: %{
|
||||
provider_id: %Schema{type: :string, format: :uuid},
|
||||
email: %Schema{type: :string, format: :email}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule Plan do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "Plan",
|
||||
type: :object,
|
||||
properties: %{
|
||||
plan_id: %Schema{type: :string},
|
||||
name: %Schema{type: :string},
|
||||
premium: %Schema{type: :number},
|
||||
coverage_details: %Schema{type: :string},
|
||||
deductible: %Schema{type: :number, nullable: true},
|
||||
coverage_limit: %Schema{type: :number, nullable: true}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule QuoteData do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "QuoteData",
|
||||
type: :object,
|
||||
properties: %{
|
||||
quote_id: %Schema{type: :string},
|
||||
valid_until: %Schema{type: :string, format: :date},
|
||||
received_at: %Schema{type: :string, format: :"date-time"},
|
||||
plans: %Schema{type: :array, items: Plan}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Requests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defmodule CreatePolicyRequest do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "CreatePolicyRequest",
|
||||
type: :object,
|
||||
required: [:policy_type, :applicant_info, :policy_details, :selected_providers],
|
||||
properties: %{
|
||||
policy_type: %Schema{
|
||||
type: :string,
|
||||
enum: ["car", "life", "fire"],
|
||||
description: "Determines the shape of policy_details"
|
||||
},
|
||||
applicant_info: ApplicantInfo,
|
||||
policy_details: PolicyDetails,
|
||||
selected_providers: %Schema{type: :array, items: SelectedProvider, minItems: 1}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule AcceptQuoteRequest do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "AcceptQuoteRequest",
|
||||
type: :object,
|
||||
required: [:quote_id, :plan_id],
|
||||
properties: %{
|
||||
quote_id: %Schema{type: :string},
|
||||
plan_id: %Schema{type: :string},
|
||||
solicitation_fields: %Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{type: :string},
|
||||
description: "Optional flat map of AcroForm field names to values",
|
||||
nullable: true
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Responses
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defmodule QuoteResponse do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "QuoteResponse",
|
||||
type: :object,
|
||||
properties: %{
|
||||
application_id: %Schema{type: :string},
|
||||
status: %Schema{type: :string}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule SolicitationUrlResponse do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "SolicitationUrlResponse",
|
||||
type: :object,
|
||||
properties: %{
|
||||
download_url: %Schema{type: :string},
|
||||
s3_key: %Schema{type: :string},
|
||||
version: %Schema{type: :integer}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule PolicySummary do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "PolicySummary",
|
||||
type: :object,
|
||||
properties: %{
|
||||
application_id: %Schema{type: :string},
|
||||
policy_type: %Schema{type: :string, enum: ["car", "life", "fire"]},
|
||||
status: %Schema{
|
||||
type: :string,
|
||||
enum: ["quote_requested", "quotes_received", "solicitation_sent", "issued"]
|
||||
},
|
||||
applicant_info: ApplicantInfo,
|
||||
policy_details: PolicyDetails,
|
||||
policy_number: %Schema{type: :string, nullable: true},
|
||||
submitted_at: %Schema{type: :string, format: :"date-time"}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule PolicyDetail do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "PolicyDetail",
|
||||
type: :object,
|
||||
properties: %{
|
||||
application_id: %Schema{type: :string},
|
||||
org_id: %Schema{type: :string},
|
||||
submitted_by: %Schema{type: :string},
|
||||
policy_type: %Schema{type: :string, enum: ["car", "life", "fire"]},
|
||||
status: %Schema{
|
||||
type: :string,
|
||||
enum: ["quote_requested", "quotes_received", "solicitation_sent", "issued"]
|
||||
},
|
||||
applicant_info: ApplicantInfo,
|
||||
policy_details: PolicyDetails,
|
||||
selected_providers: %Schema{type: :array, items: %Schema{type: :string}},
|
||||
quotes: %Schema{type: :object, additionalProperties: QuoteData},
|
||||
accepted_quote_id: %Schema{type: :string, nullable: true},
|
||||
accepted_plan_id: %Schema{type: :string, nullable: true},
|
||||
accepted_provider_id: %Schema{type: :string, nullable: true},
|
||||
accepted_at: %Schema{type: :string, format: :"date-time", nullable: true},
|
||||
solicitation_id: %Schema{type: :string, nullable: true},
|
||||
solicitation_s3_key: %Schema{type: :string, nullable: true},
|
||||
policy_number: %Schema{type: :string, nullable: true},
|
||||
premium: %Schema{type: :number, nullable: true},
|
||||
effective_date: %Schema{type: :string, format: :date, nullable: true},
|
||||
expiry_date: %Schema{type: :string, format: :date, nullable: true},
|
||||
submitted_at: %Schema{type: :string, format: :"date-time"},
|
||||
solicitation_sent_at: %Schema{type: :string, format: :"date-time", nullable: true},
|
||||
issued_at: %Schema{type: :string, format: :"date-time", nullable: true}
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule PolicyListResponse do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "PolicyListResponse",
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: %Schema{type: :array, items: PolicySummary},
|
||||
meta: PaginationMeta
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule PolicyDetailResponse do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "PolicyDetailResponse",
|
||||
type: :object,
|
||||
properties: %{
|
||||
data: PolicyDetail
|
||||
}
|
||||
})
|
||||
end
|
||||
|
||||
defmodule ErrorResponse do
|
||||
require OpenApiSpex
|
||||
|
||||
OpenApiSpex.schema(%{
|
||||
title: "ErrorResponse",
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %Schema{type: :string}
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user