dont use S3 directly allow to use any document url (document service agnostic)
All checks were successful
Build and Publish / build-release (push) Successful in 1m30s

This commit is contained in:
2026-04-29 12:18:52 -05:00
parent 8782f957b0
commit d66a8805d0
16 changed files with 78 additions and 196 deletions

View File

@@ -45,8 +45,6 @@ config :commanded_ecto_projections,
config :flop, repo: ProviderService.Repo config :flop, repo: ProviderService.Repo
config :provider_service, :s3_bucket, "provider-service"
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs" import_config "#{config_env()}.exs"

View File

@@ -17,16 +17,6 @@ config :provider_service, ProviderServiceWeb.Endpoint,
debug_errors: true, debug_errors: true,
secret_key_base: "localdevsecretkeybase1234567890localdevsecretkeybase1234567890xx" secret_key_base: "localdevsecretkeybase1234567890localdevsecretkeybase1234567890xx"
config :ex_aws,
access_key_id: "minioadmin",
secret_access_key: "minioadmin",
region: "us-east-1"
config :ex_aws, :s3,
scheme: "http://",
host: "localhost",
port: 9000
config :provider_service, ProviderService.EventStore, config :provider_service, ProviderService.EventStore,
serializer: Commanded.Serialization.JsonSerializer, serializer: Commanded.Serialization.JsonSerializer,
username: "postgres", username: "postgres",
@@ -34,8 +24,3 @@ config :provider_service, ProviderService.EventStore,
database: "provider_service_eventstore_dev", database: "provider_service_eventstore_dev",
hostname: "localhost", hostname: "localhost",
pool_size: 10 pool_size: 10
config :provider_service, :s3_bucket, "provider-service"
config :provider_service,
solicitation_service_url: "http://localhost:8081"

View File

@@ -21,21 +21,6 @@ config :logger, level: logger_level
config :logger, :console, format: {Logger.Formatter, :format} config :logger, :console, format: {Logger.Formatter, :format}
s3_host = System.get_env("S3_HOST", "dev.s3.corredorconect.com")
s3_port = System.get_env("S3_PORT", "443")
config :ex_aws,
access_key_id: System.get_env("AWS_ACCESS_KEY_ID"),
secret_access_key: System.get_env("AWS_SECRET_ACCESS_KEY"),
region: System.get_env("AWS_REGION", "us-east-1")
config :ex_aws, :s3,
scheme: "https://",
host: s3_host,
port: s3_port
config :provider_service, :s3_bucket, System.get_env("S3_BUCKET", "provider-service")
cors_origin = System.get_env("CORS_ORIGIN", "*") cors_origin = System.get_env("CORS_ORIGIN", "*")
config :cors_plug, config :cors_plug,

View File

@@ -21,7 +21,7 @@
mixFodDeps = beamPackages.fetchMixDeps { mixFodDeps = beamPackages.fetchMixDeps {
inherit pname version; inherit pname version;
src = pkgs.lib.cleanSource ./.; src = pkgs.lib.cleanSource ./.;
sha256 = "sha256-t6D0qjPNnAsYtHbwOCbuNBUwcrkvmmGf4/LeOIWgjyw="; sha256 = "sha256-NMcH0z3ME4pCFXpKBK+GjBR6Fkv0wNeQcMlQFDxNLFo=";
}; };
package = beamPackages.mixRelease { package = beamPackages.mixRelease {
inherit pname version mixFodDeps; inherit pname version mixFodDeps;

View File

@@ -98,21 +98,16 @@ defmodule ProviderService.Aggregates.Provider do
def execute(%__MODULE__{active: false}, %AddProviderTemplate{}), def execute(%__MODULE__{active: false}, %AddProviderTemplate{}),
do: {:error, :provider_inactive} do: {:error, :provider_inactive}
def execute(%__MODULE__{} = agg, %AddProviderTemplate{} = cmd) do def execute(%__MODULE__{} = agg, %AddProviderTemplate{} = cmd) do
existing = get_in(agg.templates, [cmd.policy_type, cmd.client_type]) || [] %ProviderTemplateAdded{
version = length(existing) + 1 provider_id: agg.provider_id,
template_id: cmd.template_id,
%ProviderTemplateAdded{ policy_type: cmd.policy_type,
provider_id: agg.provider_id, client_type: cmd.client_type,
template_id: cmd.template_id, document_url: cmd.document_url,
policy_type: cmd.policy_type, added_at: DateTime.utc_now()
client_type: cmd.client_type, }
s3_key: cmd.s3_key, end
fields: cmd.fields,
version: version,
added_at: DateTime.utc_now()
}
end
def execute(%__MODULE__{} = agg, %ActivateProviderTemplate{} = cmd) do def execute(%__MODULE__{} = agg, %ActivateProviderTemplate{} = cmd) do
case find_template(agg, cmd.policy_type, cmd.client_type, cmd.template_id) do case find_template(agg, cmd.policy_type, cmd.client_type, cmd.template_id) do
@@ -235,10 +230,8 @@ defmodule ProviderService.Aggregates.Provider do
[ [
%{ %{
template_id: e.template_id, template_id: e.template_id,
s3_key: e.s3_key, document_url: e.document_url,
fields: e.fields, active: false
version: e.version,
active: true
} }
] ]
) )

View File

@@ -16,7 +16,7 @@ defmodule ProviderService.Commands do
end end
defmodule AddProviderTemplate do defmodule AddProviderTemplate do
defstruct [:provider_id, :template_id, :policy_type, :s3_key, :fields, :client_type] defstruct [:provider_id, :template_id, :policy_type, :document_url, :client_type]
end end
defmodule ActivateProviderTemplate do defmodule ActivateProviderTemplate do

View File

@@ -25,9 +25,7 @@ defmodule ProviderService.Events do
:provider_id, :provider_id,
:template_id, :template_id,
:policy_type, :policy_type,
:s3_key, :document_url,
:fields,
:version,
:added_at, :added_at,
:client_type :client_type
] ]

View File

@@ -76,9 +76,7 @@ defmodule ProviderService.Projections.ProviderProjection do
template = %{ template = %{
"template_id" => e.template_id, "template_id" => e.template_id,
"client_type" => e.client_type, "client_type" => e.client_type,
"s3_key" => e.s3_key, "document_url" => e.document_url,
"fields" => e.fields || [],
"version" => e.version,
"active" => false "active" => false
} }

View File

@@ -1,9 +1,11 @@
defmodule ProviderService.Queries.ProviderQueries do defmodule ProviderService.Queries.ProviderQueries do
import Ecto.Query
alias ProviderService.Projections.Provider alias ProviderService.Projections.Provider
alias ProviderService.Repo alias ProviderService.Repo
def list_providers(params \\ %{}) do def list_providers(params \\ %{}) do
Flop.validate_and_run(Provider, params, for: Provider) base = from(p in Provider)
Flop.validate_and_run(base, params, for: Provider)
end end
def get_provider(provider_id) do def get_provider(provider_id) do

View File

@@ -1,38 +0,0 @@
defmodule ProviderService.S3 do
@bucket Application.compile_env(:provider_service, :s3_bucket)
def presigned_upload_url(s3_key) do
{:ok, url} =
ExAws.Config.new(:s3)
|> ExAws.S3.presigned_url(:put, @bucket, s3_key,
expires_in: 900,
query_params: [{"Content-Type", "application/pdf"}]
)
url
end
def presigned_download_url(s3_key) do
{:ok, url} =
ExAws.Config.new(:s3)
|> ExAws.S3.presigned_url(:get, @bucket, s3_key, expires_in: 3600)
url
end
def delete(s3_key) do
ExAws.S3.delete_object(@bucket, s3_key)
|> ExAws.request()
end
def upload(local_path, s3_key) do
local_path
|> File.read!()
|> then(&ExAws.S3.put_object(@bucket, s3_key, &1, content_type: "application/pdf"))
|> ExAws.request()
|> case do
{:ok, _} -> :ok
{:error, e} -> {:error, inspect(e)}
end
end
end

View File

@@ -13,16 +13,11 @@ defmodule ProviderServiceWeb.ProviderController do
} }
alias ProviderServiceWeb.Schemas.Provider, as: PS alias ProviderServiceWeb.Schemas.Provider, as: PS
alias ProviderServiceWeb.QueryHelpers
operation(:index, operation(:index,
summary: "List providers", summary: "List providers",
parameters: [ parameters: QueryHelpers.flop([:active, :search], [:name, :inserted_at]),
"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: [ responses: [
ok: {"Provider list", "application/json", PS.ProviderListResponse} ok: {"Provider list", "application/json", PS.ProviderListResponse}
] ]

View File

@@ -13,7 +13,6 @@ defmodule ProviderServiceWeb.TemplateController do
RemoveProviderTemplate RemoveProviderTemplate
} }
alias ProviderService.S3
alias ProviderServiceWeb.Schemas.Provider, as: PS alias ProviderServiceWeb.Schemas.Provider, as: PS
operation(:index, operation(:index,
@@ -37,65 +36,41 @@ defmodule ProviderServiceWeb.TemplateController do
end end
operation(:upload_template, operation(:upload_template,
summary: "Upload solicitation template", summary: "Register template by document URL",
description: "Upload a fillable PDF. Fields are auto-discovered via solicitation_service.", description: "Register a template document URL for a provider.",
parameters: [ parameters: [
provider_id: [in: :path, type: :string, required: true] provider_id: [in: :path, type: :string, required: true]
], ],
request_body: request_body:
{"Multipart PDF upload", "multipart/form-data", PS.UploadTemplateRequest, required: true}, {"Document URL payload", "application/json", PS.UploadTemplateRequest, required: true},
responses: [ responses: [
created: {"Template registered", "application/json", PS.UploadTemplateResponse} created: {"Template registered", "application/json", PS.UploadTemplateResponse}
] ]
) )
def upload_template(conn, %{"provider_id" => provider_id} = params) do def upload_template(conn, %{"provider_id" => provider_id} = params) do
# %Plug.Upload{} document_url = params["document_url"]
upload = params["file"]
policy_type = params["policy_type"] policy_type = params["policy_type"]
client_type = params["client_type"] client_type = params["client_type"]
template_id = Ecto.UUID.generate() template_id = Ecto.UUID.generate()
s3_key = "templates/#{provider_id}/#{policy_type}/#{client_type}/#{template_id}.pdf"
# Upload to S3/MinIO cmd = %AddProviderTemplate{
case S3.upload(upload.path, s3_key) do provider_id: provider_id,
template_id: template_id,
client_type: client_type,
policy_type: policy_type,
document_url: document_url
}
case ProviderService.CommandedApp.dispatch(cmd) do
:ok -> :ok ->
# Discover AcroForm fields via solicitation_service conn
fields = discover_fields(s3_key) |> put_status(:created)
|> json(%{template_id: template_id, document_url: document_url})
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} -> {:error, reason} ->
conn conn |> put_status(:unprocessable_entity) |> json(%{error: reason})
|> 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
end end

View File

@@ -0,0 +1,34 @@
defmodule ProviderServiceWeb.QueryHelpers do
@moduledoc false
alias OpenApiSpex.Schema
@filter_count 3
def flop(filter_fields, order_fields, other \\ []) do
[
page: [in: :query, schema: %Schema{type: :number, default: 1}],
page_size: [in: :query, schema: %Schema{type: :number, default: 20}],
order_by: [
in: :query,
schema: %Schema{type: :array, items: %Schema{type: :string, enum: order_fields}}
],
order_directions: [
in: :query,
schema: %Schema{type: :array, items: %Schema{type: :string, enum: ["asc", "desc"]}}
]
] ++ build_filter_params(filter_fields) ++ other
end
defp build_filter_params(fields) do
for i <- 0..(@filter_count - 1) do
[
{:"filters[#{i}][field]", [in: :query, schema: %Schema{type: :string, enum: fields}]},
{:"filters[#{i}][op]",
[in: :query, schema: %Schema{type: :string, enum: Flop.Filter.allowed_operators(:all)}]},
{:"filters[#{i}][value]", [in: :query, schema: %Schema{type: :string}]}
]
end
|> List.flatten()
end
end

View File

@@ -18,22 +18,6 @@ defmodule ProviderServiceWeb.Schemas.Provider do
}) })
end 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 defmodule Template do
require OpenApiSpex require OpenApiSpex
@@ -44,9 +28,7 @@ defmodule ProviderServiceWeb.Schemas.Provider do
template_id: %Schema{type: :string, format: :uuid}, template_id: %Schema{type: :string, format: :uuid},
policy_type: %Schema{type: :string, enum: ["car", "life", "fire"]}, policy_type: %Schema{type: :string, enum: ["car", "life", "fire"]},
client_type: %Schema{type: :string, enum: ["natural", "juridico"]}, client_type: %Schema{type: :string, enum: ["natural", "juridico"]},
s3_key: %Schema{type: :string}, document_url: %Schema{type: :string},
version: %Schema{type: :integer},
fields: %Schema{type: :array, items: TemplateField},
active: %Schema{type: :boolean} active: %Schema{type: :boolean}
} }
}) })
@@ -98,12 +80,11 @@ defmodule ProviderServiceWeb.Schemas.Provider do
OpenApiSpex.schema(%{ OpenApiSpex.schema(%{
title: "UploadTemplateRequest", title: "UploadTemplateRequest",
type: :object, type: :object,
required: [:file, :policy_type, :client_type], required: [:document_url, :policy_type, :client_type],
properties: %{ properties: %{
file: %Schema{ document_url: %Schema{
type: :string, type: :string,
format: :binary, description: "URL to the uploaded document"
description: "Fillable PDF (AcroForm)"
}, },
policy_type: %Schema{ policy_type: %Schema{
type: :string, type: :string,
@@ -127,8 +108,7 @@ defmodule ProviderServiceWeb.Schemas.Provider do
type: :object, type: :object,
properties: %{ properties: %{
template_id: %Schema{type: :string, format: :uuid}, template_id: %Schema{type: :string, format: :uuid},
s3_key: %Schema{type: :string}, document_url: %Schema{type: :string}
fields: %Schema{type: :array, items: TemplateField}
} }
}) })
end end

View File

@@ -46,14 +46,7 @@ defmodule ProviderService.MixProject do
{:open_api_spex, "~> 3.21"}, {:open_api_spex, "~> 3.21"},
# Pagination # Pagination
{:flop, "~> 0.26"}, {:flop, "~> 0.26"}
# AWS S3
{:ex_aws, "~> 2.5"},
{:ex_aws_s3, "~> 2.5"},
{:hackney, "~> 1.20"},
{:req, "~> 0.5"},
{:sweet_xml, "~> 0.7"}
] ]
end end

View File

@@ -43,22 +43,6 @@ controllers:
value: "*" value: "*"
PHX_HOST: "0.0.0.0" PHX_HOST: "0.0.0.0"
PHX_SERVER: "true" PHX_SERVER: "true"
S3_HOST:
value: "dev.s3.corredorconect.com"
S3_BUCKET:
value: "provider-service"
AWS_REGION:
value: "us-east-1"
AWS_ACCESS_KEY_ID:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-s3-credentials'
key: rootAccessKeyId
AWS_SECRET_ACCESS_KEY:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-s3-credentials'
key: rootSecretAccessKey
RELEASE_COOKIE: RELEASE_COOKIE:
valueFrom: valueFrom:
secretKeyRef: secretKeyRef: