init commit
All checks were successful
Build and Publish / build-release (push) Successful in 4m46s

This commit is contained in:
2026-04-15 15:31:56 -05:00
commit f566d04a04
41 changed files with 2430 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
defmodule ProviderServiceWeb.ApiSpec do
alias OpenApiSpex.{OpenApi, Info, Server}
alias OpenApiSpex.{Info, OpenApi, Paths, Server}
alias ProviderServiceWeb.{Endpoint, Router}
@behaviour OpenApiSpex.OpenApi
@impl OpenApi
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
Server.from_endpoint(Endpoint)
],
info: %Info{
title: "Provider Service",
version: "1.0"
},
# Populate the paths from a phoenix router
paths: Paths.from_router(Router)
}
# Discover request/response schemas from path specs
|> OpenApiSpex.resolve_schema_modules()
end
end

View File

@@ -0,0 +1,207 @@
defmodule ProviderServiceWeb.ProviderController do
use ProviderServiceWeb, :controller
use OpenApiSpex.ControllerSpecs
alias ProviderService.CommandedApp
alias ProviderService.Queries.ProviderQueries
alias ProviderService.Commands.{
RegisterProvider,
UpdateProvider,
DeactivateProvider,
ReactivateProvider
}
alias ProviderServiceWeb.Schemas.Provider, as: PS
operation(:index,
summary: "List providers",
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]
],
responses: [
ok: {"Provider list", "application/json", PS.ProviderListResponse}
]
)
def index(conn, params) do
case ProviderQueries.list_providers(params) do
{:ok, {providers, meta}} ->
conn
|> put_status(:ok)
|> json(%{
data: Enum.map(providers, &provider_json/1),
meta: meta_json(meta)
})
{:error, _} ->
conn |> put_status(:bad_request) |> json(%{error: "invalid parameters"})
end
end
operation(:show,
summary: "Get provider",
parameters: [
provider_id: [in: :path, type: :string, required: true]
],
responses: [
ok: {"Provider", "application/json", PS.ProviderResponse},
not_found: {"Not found", "application/json", %OpenApiSpex.Schema{type: :object}}
]
)
def show(conn, %{"provider_id" => provider_id}) do
case ProviderQueries.get_provider(provider_id) do
{:ok, provider} ->
conn |> put_status(:ok) |> json(%{data: provider_json(provider)})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "not found"})
end
end
operation(:create,
summary: "Register provider",
request_body: {"Provider data", "application/json", PS.RegisterProvider, required: true},
responses: [
created: {"Provider registered", "application/json", PS.ProviderResponse}
]
)
def create(conn, params) do
provider_id = params["provider_id"]
command = %RegisterProvider{
provider_id: provider_id,
name: params["name"],
email: params["email"],
phone: params["phone"],
contact_name: params["contact_name"],
ruc: params["ruc"],
address: params["address"]
}
case CommandedApp.dispatch(command, consistency: :strong) do
:ok ->
{:ok, provider} = ProviderQueries.get_provider(provider_id)
conn |> put_status(:created) |> json(%{data: provider_json(provider)})
{:error, reason} ->
conn |> put_status(:unprocessable_entity) |> json(%{error: inspect(reason)})
end
end
operation(:update,
summary: "Update provider",
parameters: [
provider_id: [in: :path, type: :string, required: true]
],
request_body: {"Provider data", "application/json", PS.UpdateProvider, required: true},
responses: [
ok: {"Provider updated", "application/json", PS.ProviderResponse}
]
)
def update(conn, %{"provider_id" => provider_id} = params) do
command = %UpdateProvider{
provider_id: provider_id,
name: params["name"],
email: params["email"],
phone: params["phone"],
contact_name: params["contact_name"],
ruc: params["ruc"],
address: params["address"]
}
case CommandedApp.dispatch(command, consistency: :strong) do
:ok ->
{:ok, provider} = ProviderQueries.get_provider(provider_id)
conn |> put_status(:ok) |> json(%{data: provider_json(provider)})
{:error, reason} ->
conn |> put_status(:unprocessable_entity) |> json(%{error: inspect(reason)})
end
end
operation(:deactivate,
summary: "Deactivate provider",
parameters: [
provider_id: [in: :path, type: :string, required: true]
],
responses: [
ok: {"Provider deactivated", "application/json", PS.ProviderResponse}
]
)
def deactivate(conn, %{"provider_id" => provider_id}) do
command = %DeactivateProvider{
provider_id: provider_id,
deactivated_by: "system"
}
case CommandedApp.dispatch(command, consistency: :strong) do
:ok ->
{:ok, provider} = ProviderQueries.get_provider(provider_id)
conn |> put_status(:ok) |> json(%{data: provider_json(provider)})
{:error, reason} ->
conn |> put_status(:unprocessable_entity) |> json(%{error: inspect(reason)})
end
end
operation(:reactivate,
summary: "Reactivate provider",
parameters: [
provider_id: [in: :path, type: :string, required: true]
],
responses: [
ok: {"Provider reactivated", "application/json", PS.ProviderResponse}
]
)
def reactivate(conn, %{"provider_id" => provider_id}) do
command = %ReactivateProvider{
provider_id: provider_id,
reactivated_by: "system"
}
case CommandedApp.dispatch(command, consistency: :strong) do
:ok ->
{:ok, provider} = ProviderQueries.get_provider(provider_id)
conn |> put_status(:ok) |> json(%{data: provider_json(provider)})
{:error, reason} ->
conn |> put_status(:unprocessable_entity) |> json(%{error: inspect(reason)})
end
end
defp provider_json(p) do
%{
provider_id: p.provider_id,
name: p.name,
email: p.email,
phone: p.phone,
contact_name: p.contact_name,
ruc: p.ruc,
address: p.address,
active: p.active,
templates: p.templates,
default_templates: p.default_templates
}
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
end

View File

@@ -0,0 +1,215 @@
defmodule ProviderServiceWeb.TemplateController do
use ProviderServiceWeb, :controller
use OpenApiSpex.ControllerSpecs
alias ProviderService.CommandedApp
alias ProviderService.Queries.ProviderQueries
alias ProviderService.Commands.{
AddProviderTemplate,
ActivateProviderTemplate,
DeactivateProviderTemplate,
SetDefaultProviderTemplate,
RemoveProviderTemplate
}
alias ProviderService.S3
alias ProviderServiceWeb.Schemas.Provider, as: PS
operation(:index,
summary: "List templates for provider",
parameters: [
provider_id: [in: :path, type: :string, required: true]
],
responses: [
ok: {"Templates", "application/json", %OpenApiSpex.Schema{type: :object}}
]
)
def index(conn, %{"provider_id" => provider_id}) do
case ProviderQueries.get_provider(provider_id) do
{:ok, provider} ->
conn |> put_status(:ok) |> json(%{data: provider.templates})
{:error, :not_found} ->
conn |> put_status(:not_found) |> json(%{error: "provider not found"})
end
end
operation(:upload_template,
summary: "Upload solicitation template",
description: "Upload a fillable PDF. Fields are auto-discovered via solicitation_service.",
parameters: [
provider_id: [in: :path, type: :string, required: true]
],
request_body:
{"Multipart PDF upload", "multipart/form-data", PS.UploadTemplateRequest, required: true},
responses: [
created: {"Template registered", "application/json", PS.UploadTemplateResponse}
]
)
def upload_template(conn, %{"provider_id" => provider_id} = params) do
# %Plug.Upload{}
upload = params["file"]
policy_type = params["policy_type"]
client_type = params["client_type"]
template_id = Ecto.UUID.generate()
s3_key = "templates/#{provider_id}/#{policy_type}/#{client_type}/#{template_id}.pdf"
# Upload to S3/MinIO
case S3.upload(upload.path, s3_key) do
:ok ->
# Discover AcroForm fields via solicitation_service
fields = discover_fields(s3_key)
cmd = %AddProviderTemplate{
provider_id: provider_id,
template_id: template_id,
client_type: client_type,
policy_type: policy_type,
s3_key: s3_key,
fields: fields
}
case ProviderService.CommandedApp.dispatch(cmd) do
:ok ->
conn
|> put_status(:created)
|> json(%{template_id: template_id, s3_key: s3_key, fields: fields})
{:error, reason} ->
conn |> put_status(:unprocessable_entity) |> json(%{error: reason})
end
{:error, reason} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "S3 upload failed: #{reason}"})
end
end
defp discover_fields(s3_key) do
url = Application.get_env(:provider_service, :solicitation_service_url)
case Req.get("#{url}/api/solicitations/templates/fields", params: [s3_key: s3_key]) do
{:ok, %{status: 200, body: %{"fields" => fields}}} -> fields
_ -> []
end
end
operation(:activate,
summary: "Activate a template",
parameters: [
provider_id: [in: :path, type: :string, required: true],
template_id: [in: :path, type: :string, required: true]
],
responses: [
ok: {"Template activated", "application/json", PS.ProviderResponse}
]
)
def activate(conn, %{"provider_id" => provider_id, "template_id" => template_id} = params) do
command = %ActivateProviderTemplate{
provider_id: provider_id,
template_id: template_id,
policy_type: params["policy_type"],
client_type: params["client_type"]
}
dispatch_and_respond(conn, command, provider_id)
end
operation(:deactivate,
summary: "Deactivate a template",
parameters: [
provider_id: [in: :path, type: :string, required: true],
template_id: [in: :path, type: :string, required: true]
],
responses: [
ok: {"Template deactivated", "application/json", PS.ProviderResponse}
]
)
def deactivate(conn, %{"provider_id" => provider_id, "template_id" => template_id} = params) do
command = %DeactivateProviderTemplate{
provider_id: provider_id,
template_id: template_id,
policy_type: params["policy_type"],
client_type: params["client_type"]
}
dispatch_and_respond(conn, command, provider_id)
end
operation(:set_default,
summary: "Set default template for a policy type",
parameters: [
provider_id: [in: :path, type: :string, required: true],
template_id: [in: :path, type: :string, required: true]
],
responses: [
ok: {"Default template set", "application/json", PS.ProviderResponse}
]
)
def set_default(conn, %{"provider_id" => provider_id, "template_id" => template_id} = params) do
command = %SetDefaultProviderTemplate{
provider_id: provider_id,
template_id: template_id,
policy_type: params["policy_type"],
client_type: params["client_type"]
}
dispatch_and_respond(conn, command, provider_id)
end
operation(:remove,
summary: "Remove a template",
parameters: [
provider_id: [in: :path, type: :string, required: true],
template_id: [in: :path, type: :string, required: true]
],
responses: [
ok: {"Template removed", "application/json", PS.ProviderResponse}
]
)
def remove(conn, %{"provider_id" => provider_id, "template_id" => template_id} = params) do
command = %RemoveProviderTemplate{
provider_id: provider_id,
template_id: template_id,
policy_type: params["policy_type"],
client_type: params["client_type"]
}
dispatch_and_respond(conn, command, provider_id)
end
defp dispatch_and_respond(conn, command, provider_id) do
case CommandedApp.dispatch(command, consistency: :strong) do
:ok ->
{:ok, provider} = ProviderQueries.get_provider(provider_id)
conn |> put_status(:ok) |> json(%{data: provider_json(provider)})
{:error, reason} ->
conn |> put_status(:unprocessable_entity) |> json(%{error: inspect(reason)})
end
end
defp provider_json(p) do
%{
provider_id: p.provider_id,
name: p.name,
email: p.email,
phone: p.phone,
contact_name: p.contact_name,
ruc: p.ruc,
address: p.address,
active: p.active,
templates: p.templates,
default_templates: p.default_templates
}
end
end

View File

@@ -0,0 +1,25 @@
defmodule ProviderServiceWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :provider_service
@session_options [
store: :cookie,
key: "_provider_service_key",
signing_salt: "somesalt",
same_site: "Lax"
]
plug(Plug.RequestId)
plug(Plug.Telemetry, event_prefix: [:phoenix, :endpoint])
plug(Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
)
plug(Plug.MethodOverride)
plug(Plug.Head)
plug(Plug.Session, @session_options)
plug(CORSPlug, origin: ["http://localhost:3000"])
plug(ProviderServiceWeb.Router)
end

View File

@@ -0,0 +1,20 @@
defmodule ProviderServiceWeb do
def controller do
quote do
use Phoenix.Controller, formats: [:json]
import Plug.Conn
end
end
def router do
quote do
use Phoenix.Router, helpers: false
import Plug.Conn
import Phoenix.Controller
end
end
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end

View File

@@ -0,0 +1,73 @@
defmodule ProviderServiceWeb.Router do
use Phoenix.Router
pipeline :api do
plug(:accepts, ["json"])
plug(OpenApiSpex.Plug.PutApiSpec, module: ProviderServiceWeb.ApiSpec)
end
scope "/api" do
pipe_through(:api)
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
scope "/v1" do
# Providers
get("/providers", ProviderServiceWeb.ProviderController, :index)
post("/providers", ProviderServiceWeb.ProviderController, :create)
get("/providers/:provider_id", ProviderServiceWeb.ProviderController, :show)
put("/providers/:provider_id", ProviderServiceWeb.ProviderController, :update)
post(
"/providers/:provider_id/deactivate",
ProviderServiceWeb.ProviderController,
:deactivate
)
post(
"/providers/:provider_id/reactivate",
ProviderServiceWeb.ProviderController,
:reactivate
)
# Templates
get("/providers/:provider_id/templates", ProviderServiceWeb.TemplateController, :index)
post(
"/providers/:provider_id/templates",
ProviderServiceWeb.TemplateController,
:upload_template
)
post(
"/providers/:provider_id/templates/:template_id/activate",
ProviderServiceWeb.TemplateController,
:activate
)
post(
"/providers/:provider_id/templates/:template_id/deactivate",
ProviderServiceWeb.TemplateController,
:deactivate
)
post(
"/providers/:provider_id/templates/:template_id/set-default",
ProviderServiceWeb.TemplateController,
:set_default
)
delete(
"/providers/:provider_id/templates/:template_id",
ProviderServiceWeb.TemplateController,
:remove
)
end
end
if Mix.env() == :dev do
scope "/swaggerui" do
get("/", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi")
end
end
end

View File

@@ -0,0 +1,228 @@
defmodule ProviderServiceWeb.Schemas.Provider 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
defmodule TemplateField do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "TemplateField",
type: :object,
properties: %{
field: %Schema{type: :string, example: "beneficiary_name"},
label: %Schema{type: :string, example: "Beneficiary Name"},
type: %Schema{type: :string, enum: ["string", "date", "number", "select", "boolean"]},
required: %Schema{type: :boolean},
options: %Schema{type: :array, items: %Schema{type: :string}, nullable: true}
}
})
end
defmodule Template do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Template",
type: :object,
properties: %{
template_id: %Schema{type: :string, format: :uuid},
policy_type: %Schema{type: :string, enum: ["car", "life", "fire"]},
client_type: %Schema{type: :string, enum: ["natural", "juridico"]},
s3_key: %Schema{type: :string},
version: %Schema{type: :integer},
fields: %Schema{type: :array, items: TemplateField},
active: %Schema{type: :boolean}
}
})
end
defmodule RegisterProvider do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "RegisterProvider",
type: :object,
required: [:provider_id, :name],
properties: %{
provider_id: %Schema{
type: :string,
pattern: "^[a-zA-Z0-9]+$",
description: "Alphanumeric ID for the provider"
},
name: %Schema{type: :string, example: "Seguros ABC"},
email: %Schema{type: :string, format: :email},
phone: %Schema{type: :string},
contact_name: %Schema{type: :string},
ruc: %Schema{type: :string},
address: %Schema{type: :string}
}
})
end
defmodule UpdateProvider do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "UpdateProvider",
type: :object,
properties: %{
name: %Schema{type: :string},
email: %Schema{type: :string, format: :email},
phone: %Schema{type: :string},
contact_name: %Schema{type: :string},
ruc: %Schema{type: :string},
address: %Schema{type: :string}
}
})
end
defmodule UploadTemplateRequest do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "UploadTemplateRequest",
type: :object,
required: [:file, :policy_type, :client_type],
properties: %{
file: %Schema{
type: :string,
format: :binary,
description: "Fillable PDF (AcroForm)"
},
policy_type: %Schema{
type: :string,
enum: ["car", "life", "fire"],
description: "Policy type this template applies to"
},
client_type: %Schema{
type: :string,
enum: ["natural", "juridico"],
description: "Client type this template applies to"
}
}
})
end
defmodule UploadTemplateResponse do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "UploadTemplateResponse",
type: :object,
properties: %{
template_id: %Schema{type: :string, format: :uuid},
s3_key: %Schema{type: :string},
fields: %Schema{type: :array, items: TemplateField}
}
})
end
# templates: %{ policy_type => %{ client_type => [Template] } }
# default_templates: %{ policy_type => %{ client_type => template_id } }
defmodule ClientTypeTemplates do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "ClientTypeTemplates",
type: :object,
description: "Map of client_type (natural | juridico) to list of templates",
properties: %{
natural: %Schema{type: :array, items: Template, nullable: true},
juridico: %Schema{type: :array, items: Template, nullable: true}
}
})
end
defmodule ClientTypeDefaults do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "ClientTypeDefaults",
type: :object,
description: "Map of client_type (natural | juridico) to default template_id",
properties: %{
natural: %Schema{type: :string, format: :uuid, nullable: true},
juridico: %Schema{type: :string, format: :uuid, nullable: true}
}
})
end
defmodule ProviderData do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "ProviderData",
type: :object,
properties: %{
provider_id: %Schema{type: :string, format: :uuid},
name: %Schema{type: :string},
email: %Schema{type: :string},
phone: %Schema{type: :string},
contact_name: %Schema{type: :string},
ruc: %Schema{type: :string},
address: %Schema{type: :string},
active: %Schema{type: :boolean},
templates: %Schema{
type: :object,
description: "Map of policy_type (car | life | fire) to client_type map of templates",
properties: %{
car: ClientTypeTemplates,
life: ClientTypeTemplates,
fire: ClientTypeTemplates
}
},
default_templates: %Schema{
type: :object,
description: "Map of policy_type to client_type to default template_id",
properties: %{
car: ClientTypeDefaults,
life: ClientTypeDefaults,
fire: ClientTypeDefaults
}
}
}
})
end
defmodule ProviderResponse do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "ProviderResponse",
type: :object,
properties: %{
data: ProviderData
}
})
end
defmodule ProviderListResponse do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "ProviderListResponse",
type: :object,
properties: %{
data: %Schema{type: :array, items: ProviderData},
meta: PaginationMeta
}
})
end
end