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: [ in: :query, schema: %OpenApiSpex.Schema{ type: :array, items: %OpenApiSpex.Schema{ type: :object, properties: %{ field: %OpenApiSpex.Schema{type: :string}, op: %OpenApiSpex.Schema{type: :string, default: "=="}, value: %OpenApiSpex.Schema{type: :string} } } }, required: false, style: :deepObject, explode: true ], "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), 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, applicant_info: p.applicant_info, policy_details: p.policy_details, 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, applicant_info: p.applicant_info, policy_details: p.policy_details, 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"], 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} end