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 PolicyService.Commands.LifePolicy alias PolicyService.Commands.FireStructurePolicy alias PolicyService.Commands.FireContentsPolicy alias PolicyServiceWeb.Schemas.Policy, as: S alias PolicyServiceWeb.QueryHelpers tags(["Policies"]) security([%{"bearerAuth" => []}]) operation(:index, summary: "List policies", parameters: QueryHelpers.flop( [:status, :policy_type, :search], [:submitted_at, :policy_type, :status] ), responses: [ ok: {"Policy list", "application/json", S.PolicyListResponse}, bad_request: {"Invalid params", "application/json", S.ErrorResponse} ] ) def index(conn, params) do org_id = conn.private[PolicyServiceWeb.Plugs.ExtractOrganizationId] 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.private[PolicyServiceWeb.Plugs.ExtractOrganizationId] 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.private[PolicyServiceWeb.Plugs.ExtractOrganizationId] submitted_by = conn.assigns[:user_id] with {:ok, policy_type} <- parse_policy_type(params["policy_type"]), {:ok, insured} <- parse_insured(params["insured"]), {:ok, buyer} <- parse_buyer(params["buyer"]), {:ok, insured_object} <- parse_insured_object(policy_type, params["insured_object"]), {: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, insured: insured, buyer: buyer, insured_object: insured_object, selected_providers: providers } "life" -> %LifePolicy.SubmitPolicyApplication{ id: PolicyId.new(org_id, policy_type, application_id), submitted_by: submitted_by, insured: insured, buyer: buyer, insured_object: insured_object, selected_providers: providers } "fire_structure" -> %FireStructurePolicy.SubmitPolicyApplication{ id: PolicyId.new(org_id, policy_type, application_id), submitted_by: submitted_by, insured: insured, buyer: buyer, insured_object: insured_object, selected_providers: providers } "fire_contents" -> %FireContentsPolicy.SubmitPolicyApplication{ id: PolicyId.new(org_id, policy_type, application_id), submitted_by: submitted_by, insured: insured, buyer: buyer, insured_object: insured_object, 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.private[PolicyServiceWeb.Plugs.ExtractOrganizationId] 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), accepted_by: params["accepted_by"] || "system", accepted_plan_id: params["accepted_plan_id"] } "life" -> %LifePolicy.AcceptQuoteAndSolicit{ id: PolicyId.new(org_id, policy.policy_type, application_id), accepted_by: params["accepted_by"] || "system", accepted_plan_id: params["accepted_plan_id"] } "fire_structure" -> %FireStructurePolicy.AcceptQuoteAndSolicit{ id: PolicyId.new(org_id, policy.policy_type, application_id), accepted_by: params["accepted_by"] || "system", accepted_plan_id: params["accepted_plan_id"] } "fire_contents" -> %FireContentsPolicy.AcceptQuoteAndSolicit{ id: PolicyId.new(org_id, policy.policy_type, application_id), accepted_by: params["accepted_by"] || "system", accepted_plan_id: params["accepted_plan_id"] } 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, :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 # --------------------------------------------------------------------------- # Serializers # --------------------------------------------------------------------------- defp policy_summary(p) do %{ application_id: p.application_id, policy_type: p.policy_type, status: p.status, insured: p.insured, buyer: p.buyer, insured_object: p.insured_object, provider_policy_number: p.provider_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, insured: p.insured, buyer: p.buyer, insured_object: p.insured_object, selected_providers: p.selected_providers, quotes: p.quotes, accepted_plan_id: p.accepted_plan_id, accepted_by: p.accepted_by, provider_policy_number: p.provider_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_structure", "fire_contents"], do: {:ok, type} defp parse_policy_type(_), do: {:error, :invalid_policy_type} # insured — individual defp parse_insured(info) do case info["type"] do "individual" -> case info["date_of_birth"] do nil -> {:error, :missing_date_of_birth} dob -> {:ok, %{ "type" => "individual", "name" => info["name"], "date_of_birth" => dob, "document_id" => info["document_id"], "gender" => info["gender"], "email" => info["email"], "phone" => info["phone"], "address" => info["address"] }} end "corporate" -> {:ok, %{ "type" => "corporate", "company_name" => info["company_name"], "ruc" => info["ruc"], "legal_rep_name" => info["legal_rep_name"], "legal_rep_document" => info["legal_rep_document"], "email" => info["email"], "phone" => info["phone"], "address" => info["address"] }} _ -> {:error, :invalid_insured_type} end end # buyer — individual defp parse_buyer(info) do case info["type"] do "individual" -> case info["date_of_birth"] do nil -> {:error, :missing_date_of_birth} dob -> {:ok, %{ "type" => "individual", "name" => info["name"], "date_of_birth" => dob, "document_id" => info["document_id"], "email" => info["email"], "phone" => info["phone"], "address" => info["address"] }} end "corporate" -> {:ok, %{ "type" => "corporate", "company_name" => info["company_name"], "ruc" => info["ruc"], "legal_rep_name" => info["legal_rep_name"], "legal_rep_document" => info["legal_rep_document"], "email" => info["email"], "phone" => info["phone"], "address" => info["address"] }} _ -> {:error, :invalid_buyer_type} end end # individual — has document_id # car details defp parse_insured_object("car", nil), do: {:error, :missing_insured_object} defp parse_insured_object("car", d) do {:ok, %{ "plate" => d["plate"], "make" => d["make"], "model" => d["model"], "year" => d["year"], "use_type" => d["use_type"], "car_type" => d["car_type"], "chassis_number" => d["chassis_number"], "engine_number" => d["engine_number"], "rc_limits" => %{ "bodily_injury" => d["rc_limits"]["bodily_injury"], "property_damage" => d["rc_limits"]["property_damage"] }, "market_value" => d["market_value"], "requested_value" => d["requested_value"] }} end # life details defp parse_insured_object("life", nil), do: {:error, :missing_insured_object} defp parse_insured_object("life", d) do {:ok, %{ "coverage_type" => d["coverage_type"], "coverage_amount" => d["coverage_amount"], "coverage_years" => d["coverage_years"], "smoker" => d["smoker"], "medications" => d["medications"] || [], "surgeries" => d["surgeries"] || [], "weight" => d["weight"], "height" => d["height"] }} end # fire_structure details defp parse_insured_object("fire_structure", nil), do: {:error, :missing_insured_object} defp parse_insured_object("fire_structure", d) do {:ok, %{ "location" => d["location"], "property_value" => d["property_value"], "property_use" => d["property_use"], "security_measures" => d["security_measures"] || [], "market_value" => d["market_value"] }} end # fire_contents details defp parse_insured_object("fire_contents", nil), do: {:error, :missing_insured_object} defp parse_insured_object("fire_contents", d) do {:ok, %{ "location" => d["location"], "contents_value" => d["contents_value"], "property_use" => d["property_use"], "security_measures" => d["security_measures"] || [], "high_value_items" => d["high_value_items"] || [] }} end defp parse_insured_object(_, _), do: {:error, :invalid_insured_object} 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} end