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