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,341 @@
defmodule ProviderService.Aggregates.Provider do
defstruct [
:provider_id,
:name,
:email,
:phone,
:contact_name,
:ruc,
:address,
:active,
templates: %{},
default_templates: %{}
]
alias ProviderService.Commands.{
RegisterProvider,
UpdateProvider,
DeactivateProvider,
ReactivateProvider,
AddProviderTemplate,
ActivateProviderTemplate,
DeactivateProviderTemplate,
SetDefaultProviderTemplate,
RemoveProviderTemplate
}
alias ProviderService.Events.{
ProviderRegistered,
ProviderUpdated,
ProviderDeactivated,
ProviderReactivated,
ProviderTemplateAdded,
ProviderTemplateActivated,
ProviderTemplateDeactivated,
ProviderTemplateDefaultSet,
ProviderTemplateRemoved
}
# ---------------------------------------------------------------------------
# Execute — Provider
# ---------------------------------------------------------------------------
def execute(%__MODULE__{provider_id: nil}, %RegisterProvider{} = cmd) do
%ProviderRegistered{
provider_id: cmd.provider_id,
name: cmd.name,
email: cmd.email,
phone: cmd.phone,
contact_name: cmd.contact_name,
ruc: cmd.ruc,
address: cmd.address,
registered_at: DateTime.utc_now()
}
end
def execute(%__MODULE__{active: false}, %UpdateProvider{}),
do: {:error, :provider_inactive}
def execute(%__MODULE__{} = agg, %UpdateProvider{} = cmd) do
%ProviderUpdated{
provider_id: agg.provider_id,
name: cmd.name,
email: cmd.email,
phone: cmd.phone,
contact_name: cmd.contact_name,
ruc: cmd.ruc,
address: cmd.address,
updated_at: DateTime.utc_now()
}
end
def execute(%__MODULE__{active: false}, %DeactivateProvider{}),
do: {:error, :already_inactive}
def execute(%__MODULE__{} = agg, %DeactivateProvider{} = cmd) do
%ProviderDeactivated{
provider_id: agg.provider_id,
deactivated_by: cmd.deactivated_by,
deactivated_at: DateTime.utc_now()
}
end
def execute(%__MODULE__{active: true}, %ReactivateProvider{}),
do: {:error, :already_active}
def execute(%__MODULE__{} = agg, %ReactivateProvider{} = cmd) do
%ProviderReactivated{
provider_id: agg.provider_id,
reactivated_by: cmd.reactivated_by,
reactivated_at: DateTime.utc_now()
}
end
# ---------------------------------------------------------------------------
# Execute — Templates
# ---------------------------------------------------------------------------
def execute(%__MODULE__{active: false}, %AddProviderTemplate{}),
do: {:error, :provider_inactive}
def execute(%__MODULE__{} = agg, %AddProviderTemplate{} = cmd) do
existing = get_in(agg.templates, [cmd.policy_type, cmd.client_type]) || []
version = length(existing) + 1
%ProviderTemplateAdded{
provider_id: agg.provider_id,
template_id: cmd.template_id,
policy_type: cmd.policy_type,
client_type: cmd.client_type,
s3_key: cmd.s3_key,
fields: cmd.fields,
version: version,
added_at: DateTime.utc_now()
}
end
def execute(%__MODULE__{} = agg, %ActivateProviderTemplate{} = cmd) do
case find_template(agg, cmd.policy_type, cmd.client_type, cmd.template_id) do
nil ->
{:error, :template_not_found}
_ ->
%ProviderTemplateActivated{
provider_id: agg.provider_id,
template_id: cmd.template_id,
policy_type: cmd.policy_type,
client_type: cmd.client_type,
activated_at: DateTime.utc_now()
}
end
end
def execute(%__MODULE__{} = agg, %DeactivateProviderTemplate{} = cmd) do
case find_template(agg, cmd.policy_type, cmd.client_type, cmd.template_id) do
nil ->
{:error, :template_not_found}
_ ->
%ProviderTemplateDeactivated{
provider_id: agg.provider_id,
template_id: cmd.template_id,
policy_type: cmd.policy_type,
client_type: cmd.client_type,
deactivated_at: DateTime.utc_now()
}
end
end
def execute(%__MODULE__{} = agg, %SetDefaultProviderTemplate{} = cmd) do
case find_template(agg, cmd.policy_type, cmd.client_type, cmd.template_id) do
nil ->
{:error, :template_not_found}
%{active: false} ->
{:error, :template_not_active}
_ ->
%ProviderTemplateDefaultSet{
provider_id: agg.provider_id,
template_id: cmd.template_id,
policy_type: cmd.policy_type,
client_type: cmd.client_type,
set_at: DateTime.utc_now()
}
end
end
def execute(%__MODULE__{} = agg, %RemoveProviderTemplate{} = cmd) do
case find_template(agg, cmd.policy_type, cmd.client_type, cmd.template_id) do
nil ->
{:error, :template_not_found}
_ ->
%ProviderTemplateRemoved{
provider_id: agg.provider_id,
template_id: cmd.template_id,
policy_type: cmd.policy_type,
client_type: cmd.client_type,
removed_at: DateTime.utc_now()
}
end
end
# ---------------------------------------------------------------------------
# Apply — Provider
# ---------------------------------------------------------------------------
def apply(%__MODULE__{} = agg, %ProviderRegistered{} = e) do
%__MODULE__{
agg
| provider_id: e.provider_id,
name: e.name,
email: e.email,
phone: e.phone,
contact_name: e.contact_name,
ruc: e.ruc,
address: e.address,
active: true
}
end
def apply(%__MODULE__{} = agg, %ProviderUpdated{} = e) do
%__MODULE__{
agg
| name: e.name,
email: e.email,
phone: e.phone,
contact_name: e.contact_name,
ruc: e.ruc,
address: e.address
}
end
def apply(%__MODULE__{} = agg, %ProviderDeactivated{}),
do: %__MODULE__{agg | active: false}
def apply(%__MODULE__{} = agg, %ProviderReactivated{}),
do: %__MODULE__{agg | active: true}
# ---------------------------------------------------------------------------
# Apply — Templates
# ---------------------------------------------------------------------------
def apply(%__MODULE__{} = agg, %ProviderTemplateAdded{} = e) do
existing = get_in(agg.templates, [e.policy_type, e.client_type]) || []
templates =
agg.templates
|> Map.update(e.policy_type, %{e.client_type => []}, fn inner ->
Map.update(inner, e.client_type, [], fn list -> list end)
end)
|> put_in(
[e.policy_type, e.client_type],
existing ++
[
%{
template_id: e.template_id,
s3_key: e.s3_key,
fields: e.fields,
version: e.version,
active: true
}
]
)
%__MODULE__{agg | templates: templates}
end
def apply(%__MODULE__{} = agg, %ProviderTemplateActivated{} = e) do
templates =
update_template(
agg.templates,
e.policy_type,
e.client_type,
e.template_id,
&Map.put(&1, :active, true)
)
%__MODULE__{agg | templates: templates}
end
def apply(%__MODULE__{} = agg, %ProviderTemplateDeactivated{} = e) do
templates =
update_template(
agg.templates,
e.policy_type,
e.client_type,
e.template_id,
&Map.put(&1, :active, false)
)
template_id = e.template_id
# Clear default if the deactivated template was the default
default_templates =
case get_in(agg.default_templates, [e.policy_type, e.client_type]) do
^template_id ->
update_in(agg.default_templates, [e.policy_type], &Map.delete(&1, e.client_type))
_ ->
agg.default_templates
end
%__MODULE__{agg | templates: templates, default_templates: default_templates}
end
def apply(%__MODULE__{} = agg, %ProviderTemplateDefaultSet{} = e) do
default_templates =
agg.default_templates
|> Map.update(e.policy_type, %{e.client_type => e.template_id}, fn inner ->
Map.put(inner, e.client_type, e.template_id)
end)
%__MODULE__{agg | default_templates: default_templates}
end
def apply(%__MODULE__{} = agg, %ProviderTemplateRemoved{} = e) do
templates =
agg.templates
|> update_in([e.policy_type, e.client_type], fn list ->
Enum.reject(list || [], &(&1.template_id == e.template_id))
end)
template_id = e.template_id
# Clear default if the removed template was the default
default_templates =
case get_in(agg.default_templates, [e.policy_type, e.client_type]) do
^template_id ->
update_in(agg.default_templates, [e.policy_type], &Map.delete(&1, e.client_type))
_ ->
agg.default_templates
end
%__MODULE__{agg | templates: templates, default_templates: default_templates}
end
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
# templates structure: %{ policy_type => %{ client_type => [%{template_id, ...}] } }
# default_templates: %{ policy_type => %{ client_type => template_id } }
defp find_template(agg, policy_type, client_type, template_id) do
agg.templates
|> get_in([policy_type, client_type])
|> List.wrap()
|> Enum.find(&(&1.template_id == template_id))
end
defp update_template(templates, policy_type, client_type, template_id, fun) do
templates
|> Map.update(policy_type, %{}, fn inner ->
Map.update(inner, client_type, [], fn list ->
Enum.map(list, fn t ->
if t.template_id == template_id, do: fun.(t), else: t
end)
end)
end)
end
end

View File

@@ -0,0 +1,15 @@
defmodule ProviderService.Application do
use Application
def start(_type, _args) do
children = [
ProviderService.Repo,
ProviderService.CommandedApp,
ProviderService.Projections.ProviderProjection,
ProviderServiceWeb.Endpoint
]
opts = [strategy: :one_for_one, name: ProviderService.Supervisor]
Supervisor.start_link(children, opts)
end
end

View File

@@ -0,0 +1,40 @@
defmodule ProviderService.Router do
use Commanded.Commands.Router
alias ProviderService.Aggregates.Provider
alias ProviderService.Commands.{
RegisterProvider,
UpdateProvider,
DeactivateProvider,
ReactivateProvider,
AddProviderTemplate,
ActivateProviderTemplate,
DeactivateProviderTemplate,
SetDefaultProviderTemplate,
RemoveProviderTemplate
}
identify(Provider, by: :provider_id)
dispatch(
[
RegisterProvider,
UpdateProvider,
DeactivateProvider,
ReactivateProvider,
AddProviderTemplate,
ActivateProviderTemplate,
DeactivateProviderTemplate,
SetDefaultProviderTemplate,
RemoveProviderTemplate
],
to: Provider
)
end
defmodule ProviderService.CommandedApp do
use Commanded.Application, otp_app: :provider_service
router(ProviderService.Router)
end

View File

@@ -0,0 +1,37 @@
defmodule ProviderService.Commands do
defmodule RegisterProvider do
defstruct [:provider_id, :name, :email, :phone, :contact_name, :ruc, :address]
end
defmodule UpdateProvider do
defstruct [:provider_id, :name, :email, :phone, :contact_name, :ruc, :address]
end
defmodule DeactivateProvider do
defstruct [:provider_id, :deactivated_by]
end
defmodule ReactivateProvider do
defstruct [:provider_id, :reactivated_by]
end
defmodule AddProviderTemplate do
defstruct [:provider_id, :template_id, :policy_type, :s3_key, :fields, :client_type]
end
defmodule ActivateProviderTemplate do
defstruct [:provider_id, :template_id, :policy_type, :client_type]
end
defmodule DeactivateProviderTemplate do
defstruct [:provider_id, :template_id, :policy_type, :client_type]
end
defmodule SetDefaultProviderTemplate do
defstruct [:provider_id, :template_id, :policy_type, :client_type]
end
defmodule RemoveProviderTemplate do
defstruct [:provider_id, :template_id, :policy_type, :client_type]
end
end

View File

@@ -0,0 +1,3 @@
defmodule ProviderService.EventStore do
use EventStore, otp_app: :provider_service
end

View File

@@ -0,0 +1,55 @@
defmodule ProviderService.Events do
defmodule ProviderRegistered do
@derive Jason.Encoder
defstruct [:provider_id, :name, :email, :phone, :contact_name, :ruc, :address, :registered_at]
end
defmodule ProviderUpdated do
@derive Jason.Encoder
defstruct [:provider_id, :name, :email, :phone, :contact_name, :ruc, :address, :updated_at]
end
defmodule ProviderDeactivated do
@derive Jason.Encoder
defstruct [:provider_id, :deactivated_by, :deactivated_at]
end
defmodule ProviderReactivated do
@derive Jason.Encoder
defstruct [:provider_id, :reactivated_by, :reactivated_at]
end
defmodule ProviderTemplateAdded do
@derive Jason.Encoder
defstruct [
:provider_id,
:template_id,
:policy_type,
:s3_key,
:fields,
:version,
:added_at,
:client_type
]
end
defmodule ProviderTemplateActivated do
@derive Jason.Encoder
defstruct [:provider_id, :template_id, :policy_type, :activated_at, :client_type]
end
defmodule ProviderTemplateDeactivated do
@derive Jason.Encoder
defstruct [:provider_id, :template_id, :policy_type, :deactivated_at, :client_type]
end
defmodule ProviderTemplateDefaultSet do
@derive Jason.Encoder
defstruct [:provider_id, :template_id, :policy_type, :set_at, :client_type]
end
defmodule ProviderTemplateRemoved do
@derive Jason.Encoder
defstruct [:provider_id, :template_id, :policy_type, :removed_at, :client_type]
end
end

View File

@@ -0,0 +1,34 @@
defmodule ProviderService.Projections.Provider do
use Ecto.Schema
@derive {
Flop.Schema,
filterable: [:active, :search],
sortable: [:name, :inserted_at],
default_limit: 20,
max_limit: 100,
custom_fields: [
search: [
filter: {ProviderService.Projections.ProviderFilters, :search, []},
ecto_type: :string,
operators: [:==]
]
]
}
@primary_key {:provider_id, :string, autogenerate: false}
schema "providers" do
field(:name, :string)
field(:email, :string)
field(:phone, :string)
field(:contact_name, :string)
field(:ruc, :string)
field(:address, :string)
field(:active, :boolean, default: true)
field(:templates, :map, default: %{})
field(:default_templates, :map, default: %{})
timestamps(type: :utc_datetime_usec)
end
end

View File

@@ -0,0 +1,16 @@
defmodule ProviderService.Projections.ProviderFilters do
import Ecto.Query
def search(query, %Flop.Filter{value: value}, _opts) do
term = "%#{value}%"
where(
query,
[p],
ilike(p.name, ^term) or
ilike(p.email, ^term) or
ilike(p.contact_name, ^term) or
ilike(p.ruc, ^term)
)
end
end

View File

@@ -0,0 +1,196 @@
defmodule ProviderService.Projections.ProviderProjection do
use Commanded.Projections.Ecto,
application: ProviderService.CommandedApp,
repo: ProviderService.Repo,
name: "ProviderProjection",
consistency: :strong
alias ProviderService.Events.{
ProviderRegistered,
ProviderUpdated,
ProviderDeactivated,
ProviderReactivated,
ProviderTemplateAdded,
ProviderTemplateActivated,
ProviderTemplateDeactivated,
ProviderTemplateDefaultSet,
ProviderTemplateRemoved
}
alias ProviderService.Projections.Provider
import Ecto.Query
project(%ProviderRegistered{} = e, _meta, fn multi ->
Ecto.Multi.insert(multi, :provider, %Provider{
provider_id: e.provider_id,
name: e.name,
email: e.email,
phone: e.phone,
contact_name: e.contact_name,
ruc: e.ruc,
address: e.address,
active: true,
templates: %{},
default_templates: %{}
})
end)
project(%ProviderUpdated{} = e, _meta, fn multi ->
multi
|> Ecto.Multi.run(:fetch, fn repo, _ -> {:ok, repo.get!(Provider, e.provider_id)} end)
|> Ecto.Multi.update(:provider, fn %{fetch: p} ->
Ecto.Changeset.change(p,
name: e.name,
email: e.email,
phone: e.phone,
contact_name: e.contact_name,
ruc: e.ruc,
address: e.address
)
end)
end)
project(%ProviderDeactivated{} = e, _meta, fn multi ->
multi
|> Ecto.Multi.run(:fetch, fn repo, _ -> {:ok, repo.get!(Provider, e.provider_id)} end)
|> Ecto.Multi.update(:provider, fn %{fetch: p} ->
Ecto.Changeset.change(p, active: false)
end)
end)
project(%ProviderReactivated{} = e, _meta, fn multi ->
multi
|> Ecto.Multi.run(:fetch, fn repo, _ -> {:ok, repo.get!(Provider, e.provider_id)} end)
|> Ecto.Multi.update(:provider, fn %{fetch: p} ->
Ecto.Changeset.change(p, active: true)
end)
end)
# templates: %{ policy_type => %{ client_type => [template] } }
# default_templates: %{ policy_type => %{ client_type => template_id } }
project(%ProviderTemplateAdded{} = e, _meta, fn multi ->
multi
|> Ecto.Multi.run(:fetch, fn repo, _ -> {:ok, repo.get!(Provider, e.provider_id)} end)
|> Ecto.Multi.update(:provider, fn %{fetch: p} ->
template = %{
"template_id" => e.template_id,
"client_type" => e.client_type,
"s3_key" => e.s3_key,
"fields" => e.fields || [],
"version" => e.version,
"active" => false
}
updated =
p.templates
|> Map.update(e.policy_type, %{e.client_type => [template]}, fn inner ->
Map.update(inner, e.client_type, [template], fn list -> list ++ [template] end)
end)
Ecto.Changeset.change(p, templates: updated)
end)
end)
project(%ProviderTemplateActivated{} = e, _meta, fn multi ->
multi
|> Ecto.Multi.run(:fetch, fn repo, _ -> {:ok, repo.get!(Provider, e.provider_id)} end)
|> Ecto.Multi.update(:provider, fn %{fetch: p} ->
updated =
update_template_field(
p.templates,
e.policy_type,
e.client_type,
e.template_id,
"active",
true
)
Ecto.Changeset.change(p, templates: updated)
end)
end)
project(%ProviderTemplateDeactivated{} = e, _meta, fn multi ->
multi
|> Ecto.Multi.run(:fetch, fn repo, _ -> {:ok, repo.get!(Provider, e.provider_id)} end)
|> Ecto.Multi.update(:provider, fn %{fetch: p} ->
updated =
update_template_field(
p.templates,
e.policy_type,
e.client_type,
e.template_id,
"active",
false
)
template_id = e.template_id
default_templates =
case get_in(p.default_templates, [e.policy_type, e.client_type]) do
^template_id ->
Map.update(p.default_templates, e.policy_type, %{}, &Map.delete(&1, e.client_type))
_ ->
p.default_templates
end
Ecto.Changeset.change(p, templates: updated, default_templates: default_templates)
end)
end)
project(%ProviderTemplateDefaultSet{} = e, _meta, fn multi ->
multi
|> Ecto.Multi.run(:fetch, fn repo, _ -> {:ok, repo.get!(Provider, e.provider_id)} end)
|> Ecto.Multi.update(:provider, fn %{fetch: p} ->
default_templates =
p.default_templates
|> Map.update(e.policy_type, %{e.client_type => e.template_id}, fn inner ->
Map.put(inner, e.client_type, e.template_id)
end)
Ecto.Changeset.change(p, default_templates: default_templates)
end)
end)
project(%ProviderTemplateRemoved{} = e, _meta, fn multi ->
multi
|> Ecto.Multi.run(:fetch, fn repo, _ -> {:ok, repo.get!(Provider, e.provider_id)} end)
|> Ecto.Multi.update(:provider, fn %{fetch: p} ->
updated =
p.templates
|> Map.update(e.policy_type, %{}, fn inner ->
Map.update(inner, e.client_type, [], fn list ->
Enum.reject(list, &(&1["template_id"] == e.template_id))
end)
end)
template_id = e.template_id
default_templates =
case get_in(p.default_templates, [e.policy_type, e.client_type]) do
^template_id ->
Map.update(p.default_templates, e.policy_type, %{}, &Map.delete(&1, e.client_type))
_ ->
p.default_templates
end
Ecto.Changeset.change(p, templates: updated, default_templates: default_templates)
end)
end)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
defp update_template_field(templates, policy_type, client_type, template_id, field, value) do
Map.update(templates, policy_type, %{}, fn inner ->
Map.update(inner, client_type, [], fn list ->
Enum.map(list, fn t ->
if t["template_id"] == template_id, do: Map.put(t, field, value), else: t
end)
end)
end)
end
end

View File

@@ -0,0 +1,18 @@
defmodule ProviderService do
@moduledoc """
Documentation for `ProviderService`.
"""
@doc """
Hello world.
## Examples
iex> ProviderService.hello()
:world
"""
def hello do
:world
end
end

View File

@@ -0,0 +1,34 @@
defmodule ProviderService.Queries.ProviderQueries do
alias ProviderService.Projections.Provider
alias ProviderService.Repo
def list_providers(params \\ %{}) do
Flop.validate_and_run(Provider, params, for: Provider)
end
def get_provider(provider_id) do
case Repo.get(Provider, provider_id) do
nil -> {:error, :not_found}
provider -> {:ok, provider}
end
end
def get_active_template(provider_id, policy_type) do
with {:ok, provider} <- get_provider(provider_id) do
default_id = Map.get(provider.default_templates, policy_type)
templates = Map.get(provider.templates, policy_type, [])
result =
if default_id do
Enum.find(templates, &(&1["template_id"] == default_id))
else
Enum.find(templates, &(&1["active"] == true))
end
case result do
nil -> {:error, :no_active_template}
template -> {:ok, template}
end
end
end
end

View File

@@ -0,0 +1,5 @@
defmodule ProviderService.Repo do
use Ecto.Repo,
otp_app: :provider_service,
adapter: Ecto.Adapters.Postgres
end

View File

@@ -0,0 +1,38 @@
defmodule ProviderService.S3 do
@bucket Application.compile_env(:provider_service, :s3_bucket, "policy-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