Compare commits

...

95 Commits

Author SHA1 Message Date
a83563a576 move cors up
All checks were successful
Build and Publish / build-release (push) Successful in 1m13s
2026-05-14 10:40:48 -05:00
c3eb2471dc allow all
All checks were successful
Build and Publish / build-release (push) Successful in 3m17s
2026-05-14 10:20:32 -05:00
90f8ef00fa set cors in api pipeline
Some checks failed
Build and Publish / build-release (push) Failing after 10s
2026-05-14 10:16:50 -05:00
cd0c0b21b4 add corsplug
All checks were successful
Build and Publish / build-release (push) Successful in 1m12s
2026-05-13 17:57:24 -05:00
39f5671b2c use correct org_id
All checks were successful
Build and Publish / build-release (push) Successful in 1m12s
2026-05-13 17:51:32 -05:00
8b979f7956 fix roles claim
All checks were successful
Build and Publish / build-release (push) Successful in 1m13s
2026-05-13 17:48:30 -05:00
f829088b5b use keyword methods
All checks were successful
Build and Publish / build-release (push) Successful in 1m34s
2026-05-13 17:39:00 -05:00
1692fa29da fix keyword
All checks were successful
Build and Publish / build-release (push) Successful in 1m12s
2026-05-13 17:34:59 -05:00
921a9da748 merge keywoards
All checks were successful
Build and Publish / build-release (push) Successful in 1m10s
2026-05-13 17:32:47 -05:00
2e6784b50b cmon
All checks were successful
Build and Publish / build-release (push) Successful in 1m16s
2026-05-13 17:24:01 -05:00
47385cf827 deconstruct
All checks were successful
Build and Publish / build-release (push) Successful in 1m14s
2026-05-13 17:19:04 -05:00
9e6a9e4a48 fix auth
All checks were successful
Build and Publish / build-release (push) Successful in 1m10s
2026-05-13 17:14:57 -05:00
db732c0af0 remove corsplug 2026-05-13 16:32:27 -05:00
0957c18d21 forgor comma
All checks were successful
Build and Publish / build-release (push) Successful in 1m15s
2026-05-13 16:26:26 -05:00
b2f6fc3d86 fix undefined
All checks were successful
Build and Publish / build-release (push) Successful in 1m9s
2026-05-13 16:24:05 -05:00
3d66db2386 configure introspection correctly
Some checks failed
Build and Publish / build-release (push) Failing after 28s
2026-05-13 16:22:46 -05:00
6552e22121 add x-organization-id
All checks were successful
Build and Publish / build-release (push) Successful in 1m8s
2026-05-13 16:00:28 -05:00
ce2c038657 fix names
All checks were successful
Build and Publish / build-release (push) Successful in 1m10s
2026-05-13 15:42:25 -05:00
a872d33957 use validate
All checks were successful
Build and Publish / build-release (push) Successful in 1m7s
2026-05-13 15:39:52 -05:00
4d7d304c47 allow all headers
All checks were successful
Build and Publish / build-release (push) Successful in 1m13s
2026-05-13 15:00:20 -05:00
acc858cc1f init opts too
All checks were successful
Build and Publish / build-release (push) Successful in 1m21s
2026-05-13 14:31:54 -05:00
03db2de510 use keywoard list
All checks were successful
Build and Publish / build-release (push) Successful in 1m10s
2026-05-13 14:26:40 -05:00
6a68c348bd use plug call directly
All checks were successful
Build and Publish / build-release (push) Successful in 1m12s
2026-05-13 14:23:11 -05:00
e235190d4e get config add runtime
All checks were successful
Build and Publish / build-release (push) Successful in 1m11s
2026-05-13 14:14:49 -05:00
a925bf49c8 remove toplevel zitadel
All checks were successful
Build and Publish / build-release (push) Successful in 1m11s
2026-05-13 13:38:53 -05:00
1555e25e85 use get_env
All checks were successful
Build and Publish / build-release (push) Successful in 1m14s
2026-05-13 13:33:21 -05:00
ae4ce33acf fix config
All checks were successful
Build and Publish / build-release (push) Successful in 1m15s
2026-05-13 13:21:20 -05:00
20d5e86975 refactor auth
Some checks failed
Build and Publish / build-release (push) Failing after 1m49s
2026-05-13 13:04:31 -05:00
07a232c131 add rbacs
All checks were successful
Build and Publish / build-release (push) Successful in 2m4s
2026-05-07 14:01:18 -05:00
9439c62507 use auth endpoints
All checks were successful
Build and Publish / build-release (push) Successful in 1m52s
2026-05-07 12:23:15 -05:00
1f73fe75cc add cacertficates
All checks were successful
Build and Publish / build-release (push) Successful in 1m44s
2026-05-06 14:14:42 -05:00
15d3e5a089 sleep
Some checks failed
Build and Publish / build-release (push) Failing after 39s
2026-05-06 12:30:39 -05:00
ce43422892 remove trailing slash
Some checks failed
Build and Publish / build-release (push) Failing after 1m41s
2026-05-05 16:49:27 -05:00
4a63be5873 add extra commands
Some checks failed
Build and Publish / build-release (push) Failing after 1m5s
2026-05-05 16:47:48 -05:00
f658cb6e7f set ERL_SSL_PATH
All checks were successful
Build and Publish / build-release (push) Successful in 2m8s
2026-05-05 16:39:09 -05:00
2b04a4c620 set env for ssl
All checks were successful
Build and Publish / build-release (push) Successful in 2m20s
2026-05-05 15:51:32 -05:00
b931622c8f add cacert
All checks were successful
Build and Publish / build-release (push) Successful in 2m30s
2026-05-05 15:42:27 -05:00
9cfa7ae743 remove policy issued
All checks were successful
Build and Publish / build-release (push) Successful in 2m24s
2026-05-05 15:32:33 -05:00
99adff5da9 remove oidc for now
All checks were successful
Build and Publish / build-release (push) Successful in 2m20s
2026-05-05 15:28:04 -05:00
3ec95913fd fix values
All checks were successful
Build and Publish / build-release (push) Successful in 1m35s
2026-05-05 14:42:13 -05:00
2137cf4959 make provider config simpler
All checks were successful
Build and Publish / build-release (push) Successful in 1m30s
2026-05-04 16:06:19 -05:00
44d89014fd add authentication with zitadel
Some checks failed
Build and Publish / build-release (push) Failing after 1m49s
2026-05-04 15:52:09 -05:00
636d456c24 fix titles
Some checks failed
Build and Publish / build-release (push) Has been cancelled
2026-04-30 13:37:51 -05:00
dfce7873fb rename policy_details to insured_object
All checks were successful
Build and Publish / build-release (push) Successful in 1m36s
2026-04-30 13:13:41 -05:00
42cb25a3b6 fix search and insured override
All checks were successful
Build and Publish / build-release (push) Successful in 1m50s
2026-04-30 11:35:27 -05:00
b5686f890a add life policy aggregate
Some checks failed
Build and Publish / build-release (push) Failing after 38s
2026-04-29 16:56:51 -05:00
5a98549a24 consume commands correctly
All checks were successful
Build and Publish / build-release (push) Successful in 1m27s
2026-04-27 14:14:19 -05:00
2a8f2ffc2d refactor buyer and insured and add more policy types
All checks were successful
Build and Publish / build-release (push) Successful in 1m38s
2026-04-27 14:06:28 -05:00
c8a58c3f58 add solictation consumer
All checks were successful
Build and Publish / build-release (push) Successful in 1m38s
2026-04-23 10:32:20 -05:00
33bba5b453 fix plan tuple
All checks were successful
Build and Publish / build-release (push) Successful in 1m47s
2026-04-22 16:40:28 -05:00
82a92a9116 fix schema
All checks were successful
Build and Publish / build-release (push) Successful in 1m23s
2026-04-22 16:37:53 -05:00
2adb948b8e fix applicant and filters
All checks were successful
Build and Publish / build-release (push) Successful in 1m24s
2026-04-22 16:24:20 -05:00
b78a7fdf73 normalize query helpers
All checks were successful
Build and Publish / build-release (push) Successful in 1m27s
2026-04-22 15:22:36 -05:00
0b53afd832 make query helpers default with flop format
Some checks failed
Build and Publish / build-release (push) Failing after 37s
2026-04-22 14:53:49 -05:00
4383080696 use new query helper
All checks were successful
Build and Publish / build-release (push) Successful in 1m25s
2026-04-22 14:39:00 -05:00
e209879898 test out this format
Some checks failed
Build and Publish / build-release (push) Failing after 37s
2026-04-22 14:10:44 -05:00
0af709f7b0 properly add filters
All checks were successful
Build and Publish / build-release (push) Successful in 1m26s
2026-04-22 13:40:57 -05:00
a7160aadcf revamp aggregate and use typestruct
All checks were successful
Build and Publish / build-release (push) Successful in 1m41s
2026-04-22 11:37:04 -05:00
5f2f9e9085 fix logic
All checks were successful
Build and Publish / build-release (push) Successful in 1m25s
2026-04-21 15:15:49 -05:00
dd2e25e86a fix pattern match
All checks were successful
Build and Publish / build-release (push) Successful in 1m23s
2026-04-21 15:13:20 -05:00
a5e3e1140e add dialyzer
All checks were successful
Build and Publish / build-release (push) Successful in 1m48s
2026-04-21 14:28:33 -05:00
30d95f18e1 change queue name
All checks were successful
Build and Publish / build-release (push) Successful in 1m35s
2026-04-21 11:55:06 -05:00
089e8fc900 consume returns ok
All checks were successful
Build and Publish / build-release (push) Successful in 1m22s
2026-04-20 15:50:13 -05:00
2fac2306e4 fix routing key
All checks were successful
Build and Publish / build-release (push) Successful in 1m29s
2026-04-20 15:31:24 -05:00
079837a20b fix policy_id and use quotetaskconsumer
All checks were successful
Build and Publish / build-release (push) Successful in 1m33s
2026-04-20 14:43:17 -05:00
f59c9cadfa properly decode policy id
Some checks failed
Build and Publish / build-release (push) Failing after 35s
2026-04-17 12:57:35 -05:00
1bb457c145 disable consumers for now
All checks were successful
Build and Publish / build-release (push) Successful in 1m33s
2026-04-17 10:36:56 -05:00
1fc2b50fbc fix exchange names
All checks were successful
Build and Publish / build-release (push) Successful in 1m24s
2026-04-16 17:00:22 -05:00
8fbb91b0dd publish on quote_requested
All checks were successful
Build and Publish / build-release (push) Successful in 1m23s
2026-04-16 16:50:05 -05:00
0a3d63317b fix messaging bus
All checks were successful
Build and Publish / build-release (push) Successful in 1m27s
2026-04-16 16:35:43 -05:00
9da5817f61 fix cors_plug
All checks were successful
Build and Publish / build-release (push) Successful in 1m21s
2026-04-16 16:07:04 -05:00
ccb282251a remove resources for now
All checks were successful
Build and Publish / build-release (push) Successful in 1m26s
2026-04-16 15:34:10 -05:00
3693e11811 fix vhost
All checks were successful
Build and Publish / build-release (push) Successful in 1m49s
2026-04-16 15:09:27 -05:00
c485d37b6b fix exchanges
All checks were successful
Build and Publish / build-release (push) Successful in 1m25s
2026-04-16 14:55:34 -05:00
ec69df8e95 fix secretKeyRef
All checks were successful
Build and Publish / build-release (push) Successful in 1m24s
2026-04-16 12:25:21 -05:00
e64238df08 add proper release and migrations
All checks were successful
Build and Publish / build-release (push) Successful in 1m48s
2026-04-16 12:21:04 -05:00
d42e212bd4 use proper credentials
All checks were successful
Build and Publish / build-release (push) Successful in 1m30s
2026-04-15 14:20:23 -05:00
040a1ccfeb remove ruoute
All checks were successful
Build and Publish / build-release (push) Successful in 1m26s
2026-04-15 14:14:04 -05:00
083c8eebf2 add namespace
All checks were successful
Build and Publish / build-release (push) Successful in 1m26s
2026-04-15 13:51:36 -05:00
24d8a58bbc add compose
All checks were successful
Build and Publish / build-release (push) Successful in 1m28s
2026-04-15 13:44:53 -05:00
d98c219398 add route and compose rabbit url
Some checks failed
Build and Publish / build-release (push) Has been cancelled
2026-04-15 13:36:00 -05:00
7164f07d3a always expose swaggerUI
All checks were successful
Build and Publish / build-release (push) Successful in 1m32s
2026-04-15 12:33:45 -05:00
2c8000c757 fix path for health endpoint
All checks were successful
Build and Publish / build-release (push) Successful in 4m18s
2026-04-15 12:14:31 -05:00
1acd340dfb add user-permission
All checks were successful
Build and Publish / build-release (push) Successful in 4m17s
2026-04-15 12:06:24 -05:00
663ce94318 remove bad flags in vm
All checks were successful
Build and Publish / build-release (push) Successful in 4m4s
2026-04-15 11:47:08 -05:00
f8ab0d7488 ad vm flags
All checks were successful
Build and Publish / build-release (push) Successful in 4m8s
2026-04-15 11:23:47 -05:00
dafc21a92a add rabbitmq user
All checks were successful
Build and Publish / build-release (push) Successful in 2m41s
2026-04-14 16:45:00 -05:00
4276fc81d5 set phx_server
All checks were successful
Build and Publish / build-release (push) Successful in 2m45s
2026-04-14 16:28:52 -05:00
d289da18fa set amq
Some checks failed
Build and Publish / build-release (push) Has been cancelled
2026-04-14 16:28:37 -05:00
4e7db4a7c5 remove resources from cluster
All checks were successful
Build and Publish / build-release (push) Successful in 3m14s
2026-04-14 16:10:52 -05:00
f0e592a220 fix pool sizes
All checks were successful
Build and Publish / build-release (push) Successful in 2m38s
2026-04-14 16:05:41 -05:00
f06eeb13c8 fix limits
All checks were successful
Build and Publish / build-release (push) Successful in 4m24s
2026-04-14 15:41:41 -05:00
8e4bb5c4e5 indent in spec
All checks were successful
Build and Publish / build-release (push) Successful in 3m52s
2026-04-14 15:31:06 -05:00
822c133ca3 remove mix
All checks were successful
Build and Publish / build-release (push) Successful in 2m36s
2026-04-14 15:24:28 -05:00
27801d9f2d use external secrets for password generation
Some checks failed
Build and Publish / build-release (push) Failing after 6s
2026-04-14 15:23:02 -05:00
47 changed files with 1798 additions and 653 deletions

67
.gitignore vendored
View File

@@ -1,3 +1,6 @@
/.direnv/
/result
# The directory Mix will write compiled artifacts to.
/_build/
@@ -23,5 +26,67 @@ erl_crash.dump
/tmp/
# Ignore package tarball (built via "mix hex.build").
policy_service-*.tar
plausible-*.tar
# If NPM crashes, it generates a log, let's ignore it too.
npm-debug.log
# If running Clickhouse through the Makefile, its data is written here
/.clickhouse_db_vol/
# The directory NPM downloads your dependencies sources to.
/assets/node_modules/
/tracker/node_modules/
# Files generated by Playwright when running tracker tests
/tracker/test-results/
/tracker/playwright-report/
/tracker/blob-report/
/tracker/playwright/.cache/
# Stored hash of source tracker files used in development environment
# to detect changes in /tracker/src and avoid unnecessary compilation.
/tracker/compiler/last-hash.txt
# Temporary file used by analyze-sizes.js
/tracker/compiler/.analyze-sizes.json
# Tracker npm module files that are generated by the compiler for the NPM package
/tracker/npm_package/plausible.js*
/tracker/npm_package/plausible.cjs*
/tracker/npm_package/plausible.d.cts
# test coverage directory
/assets/coverage
# Since we are building assets from assets/,
# we ignore priv/static. You may want to comment
# this depending on your deployment strategy.
/priv/static/css
/priv/static/js
/priv/version.json
# Files matching config/*.secret.exs pattern contain sensitive
# data and you should not commit them into version control.
#
# Alternatively, you may comment the line below and commit the
# secrets files as long as you replace their contents by environment
# variables.
/config/*.secret.exs
# Ignore Elixir Language Server files
.elixir_ls
plausible-report.xml
.idea
*.iml
*.log
*.code-workspace
.vscode
# Dializer
/priv/plts/*.plt
/priv/plts/*.plt.hash
.env
.claude

View File

@@ -74,6 +74,13 @@ config :phoenix, :plug_init_mode, :runtime
config :open_api_spex, :cache_adapter, OpenApiSpex.Plug.NoneCache
config :policy_service,
provider_service_url: "http://localhost:4002",
solicitation_service_url: "http://localhost:8081"
config :policy_service, :zitadel,
issuer: System.get_env("ZITADEL_ISSUER", "https://id.corredorconect.com"),
client_id: System.get_env("ZITADEL_CLIENT_ID"),
client_secret: System.get_env("ZITADEL_CLIENT_SECRET"),
roles_claim: "urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:roles",
required_scopes: [
"openid",
"profile",
"urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:roles"
]

View File

@@ -1,7 +1,6 @@
import Config
# Do not print debug messages in production
config :logger, level: :info
config :logger, level: :debug
# Runtime production configuration, including reading
# of environment variables, is done on config/runtime.exs.

View File

@@ -7,6 +7,36 @@ import Config
# any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration.
logger_level =
case System.get_env("LOG_LEVEL", "info") do
"debug" -> :debug
"info" -> :info
"warn" -> :warning
"error" -> :error
val when val in ["warning", "error"] -> :error
_ -> :info
end
config :logger, level: logger_level
config :logger, :console, format: {Logger.Formatter, :format}
rabbitmq_host = System.get_env("RABBITMQ_HOST", "localhost")
rabbitmq_vhost = System.get_env("RABBITMQ_VHOST", "/")
rabbitmq_username = System.get_env("RABBITMQ_USERNAME")
rabbitmq_password = System.get_env("RABBITMQ_PASSWORD")
amqp_url =
if rabbitmq_username && rabbitmq_password do
"amqp://#{rabbitmq_username}:#{rabbitmq_password}@#{rabbitmq_host}/#{rabbitmq_vhost}"
end
if amqp_url do
config :policy_service, :amqp_url, amqp_url
end
# Zitadel Configuration
# ## Using releases
#
# If you use `mix release`, you need to explicitly enable the server
@@ -20,8 +50,9 @@ if System.get_env("PHX_SERVER") do
config :policy_service, PolicyServiceWeb.Endpoint, server: true
end
config :policy_service, PolicyServiceWeb.Endpoint,
http: [port: String.to_integer(System.get_env("PORT", "4000"))]
if cookie = System.get_env("RELEASE_COOKIE") do
config :elixir, :cookie, cookie
end
if config_env() == :prod do
database_url =
@@ -35,13 +66,14 @@ if config_env() == :prod do
config :policy_service, PolicyService.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
pool_size: String.to_integer(System.get_env("DATABASE_POOL_SIZE") || "1"),
socket_options: maybe_ipv6
config :policy_service, PolicyService.EventStore,
serializer: Commanded.Serialization.JsonSerializer,
url: database_url,
pool_size: 5
schema: "eventstore",
pool_size: String.to_integer(System.get_env("EVENTSTORE_POOL_SIZE") || "1")
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
@@ -55,9 +87,22 @@ if config_env() == :prod do
config :policy_service, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
config :policy_service, PolicyServiceWeb.Endpoint,
url: [host: host, port: 80, scheme: "http"],
url: [host: host, port: String.to_integer(System.get_env("PORT", "4000")), scheme: "http"],
http: [
ip: {0, 0, 0, 0, 0, 0, 0, 0}
ip: {0, 0, 0, 0, 0, 0, 0, 0},
port: String.to_integer(System.get_env("PORT", "4000"))
],
secret_key_base: secret_key_base
config :policy_service, :zitadel,
issuer: System.get_env("ZITADEL_ISSUER", "https://id.corredorconect.com"),
client_id: System.get_env("ZITADEL_CLIENT_ID"),
client_secret: System.get_env("ZITADEL_CLIENT_SECRET"),
roles_claim: "urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:roles",
required_scopes: [
"openid",
"profile",
"urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:roles",
"urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:aud"
]
end

View File

@@ -21,7 +21,7 @@
mixFodDeps = beamPackages.fetchMixDeps {
inherit pname version;
src = ./.;
sha256 = "sha256-yqxq5pB7dKEhCZiJWXrKKCra45hxfyyfpP/zyNLEF7A=";
sha256 = "sha256-YqPo8102nqTd6cAxt6O2R+nLLa9UfRLza4qojxoMtqM=";
};
package = beamPackages.mixRelease {
inherit pname version mixFodDeps;
@@ -29,13 +29,13 @@
meta = {
mainProgram = "policy_service";
};
removeCookie = false;
};
dockerImage = pkgs.dockerTools.buildLayeredImage {
name = "policy_service";
contents = [ package pkgs.busybox pkgs.shadow ];
contents = [ package pkgs.bashInteractive pkgs.busybox pkgs.dockerTools.caCertificates ];
config = {
Cmd = [ "${package}/bin/policy_service" "start" ];
Entrypoint = [ "/bin/sh" ];
};
};
in
@@ -48,6 +48,7 @@
elixir-ls
kubernetes-helm
git
nodejs
];
};
}

View File

@@ -6,29 +6,60 @@ defmodule PolicyService.Aggregates.CarPolicyApplication do
@valid_use_types ~w(private commercial bus taxi school)
@valid_car_types ~w(sedan suv hatchback coupe convertible pickup van minivan truck)
def validate_details(%{
def validate_insured_object(%{
"plate" => plate,
"make" => make,
"model" => model,
"year" => year,
"use_type" => use_type,
"car_type" => car_type,
"rc_limits" => _rc_limits,
"market_value" => market_value,
"requested_value" => requested_value
})
when is_binary(plate) and is_binary(make) and is_binary(model) and
is_integer(year) and
is_number(market_value) and market_value > 0 and
is_number(requested_value) and requested_value > 0 and
is_binary(use_type) and byte_size(use_type) > 0 and
is_binary(car_type) and byte_size(car_type) > 0 do
cond do
year < 1886 or year > Date.utc_today().year + 1 -> {:error, :invalid_car_year}
use_type not in @valid_use_types -> {:error, :invalid_use_type}
car_type not in @valid_car_types -> {:error, :invalid_car_type}
byte_size(plate) == 0 -> {:error, :missing_plate}
true -> :ok
end
end
def validate_insured_object(%{
"plate" => plate,
"make" => make,
"model" => model,
"year" => year,
"car_value" => car_value,
"use_type" => use_type,
"car_type" => car_type,
"chassis_number" => chassis,
"engine_number" => engine
"engine_number" => engine,
"rc_limits" => _rc_limits,
"market_value" => market_value,
"requested_value" => requested_value
})
when is_binary(plate) and is_binary(make) and is_binary(model) and
is_integer(year) and is_number(car_value) and car_value > 0 and
is_integer(year) and
is_number(market_value) and market_value > 0 and
is_number(requested_value) and requested_value > 0 and
is_binary(use_type) and byte_size(use_type) > 0 and
is_binary(car_type) and byte_size(car_type) > 0 and
is_binary(chassis) and is_binary(engine) do
cond do
year < 1886 or year > Date.utc_today().year + 1 -> {:error, :invalid_car_year}
use_type not in @valid_use_types -> {:error, :invalid_use_type}
car_type not in @valid_car_types -> {:error, :invalid_car_type}
byte_size(chassis) == 0 -> {:error, :missing_chassis_number}
byte_size(engine) == 0 -> {:error, :missing_engine_number}
byte_size(plate) == 0 -> {:error, :missing_plate}
true -> :ok
end
end
def validate_details(_), do: {:error, :invalid_car_details}
def validate_insured_object(_), do: {:error, :invalid_car_details}
end

View File

@@ -0,0 +1,22 @@
defmodule PolicyService.Aggregates.FireContentsPolicyApplication do
use PolicyService.Aggregates.PolicyApplication,
policy_type: "fire_contents",
commands: PolicyService.Commands.FireContentsPolicy
def validate_insured_object(%{
"location" => location,
"contents_value" => value,
"property_use" => use_type,
"security_measures" => measures,
"high_value_items" => items
})
when is_binary(location) and byte_size(location) > 0 and
is_number(value) and value > 0 and
is_binary(use_type) and byte_size(use_type) > 0 and
is_list(measures) and
is_list(items) do
:ok
end
def validate_insured_object(_), do: {:error, :invalid_fire_contents_details}
end

View File

@@ -1,11 +0,0 @@
defmodule PolicyService.Aggregates.FirePolicyApplication do
use PolicyService.Aggregates.PolicyApplication,
policy_type: "fire",
commands: PolicyService.Commands.FirePolicy
def validate_details(%{property_address: addr, property_value: val})
when is_binary(addr) and byte_size(addr) > 0 and is_number(val) and val > 0,
do: :ok
def validate_details(_), do: {:error, :invalid_fire_details}
end

View File

@@ -0,0 +1,22 @@
defmodule PolicyService.Aggregates.FireStructurePolicyApplication do
use PolicyService.Aggregates.PolicyApplication,
policy_type: "fire_structure",
commands: PolicyService.Commands.FireStructurePolicy
def validate_insured_object(%{
"location" => location,
"property_value" => value,
"property_use" => use_type,
"security_measures" => measures,
"market_value" => market_value
})
when is_binary(location) and byte_size(location) > 0 and
is_number(value) and value > 0 and
is_binary(use_type) and byte_size(use_type) > 0 and
is_list(measures) and
is_number(market_value) and market_value > 0 do
:ok
end
def validate_insured_object(_), do: {:error, :invalid_fire_structure_details}
end

View File

@@ -0,0 +1,44 @@
defmodule PolicyService.Aggregates.LifePolicyApplication do
use PolicyService.Aggregates.PolicyApplication,
policy_type: "life",
commands: PolicyService.Commands.LifePolicy
@valid_coverage_types ~w(banking protection)
def validate_insured_object(%{
"coverage_type" => coverage_type,
"coverage_amount" => coverage_amount,
"coverage_years" => coverage_years,
"smoker" => smoker,
"medications" => medications,
"surgeries" => surgeries,
"weight" => weight,
"height" => height
})
when is_binary(coverage_type) and byte_size(coverage_type) > 0 and
is_number(coverage_amount) and coverage_amount > 0 and
is_integer(coverage_years) and coverage_years > 0 and
is_boolean(smoker) and
is_list(medications) and
is_list(surgeries) and
is_number(weight) and weight > 0 and
is_number(height) and height > 0 do
cond do
coverage_type not in @valid_coverage_types -> {:error, :invalid_coverage_type}
coverage_years > 100 -> {:error, :invalid_coverage_years}
true -> :ok
end
end
def validate_insured_object(_), do: {:error, :invalid_life_details}
def validate_insured(%{"type" => "corporate"}),
do: {:error, :life_insurance_requires_individual}
def validate_insured(%{"type" => "individual", "gender" => gender} = insured)
when is_binary(gender) and byte_size(gender) > 0 do
super(insured)
end
def validate_insured(%{"type" => "individual"}), do: {:error, :missing_gender}
end

View File

@@ -1,7 +1,7 @@
defmodule PolicyService.Aggregates.PolicyApplication do
@moduledoc """
Behaviour and __using__ macro for policy application aggregates.
Each policy type implements validate_details/1 and declares its detail fields.
Each policy type implements validate_insured_object/1 and declares its insured object fields.
Usage:
defmodule PolicyService.Aggregates.CarPolicyApplication do
@@ -10,7 +10,7 @@ defmodule PolicyService.Aggregates.PolicyApplication do
end
"""
@callback validate_details(map()) :: :ok | {:error, term()}
@callback validate_insured_object(map()) :: :ok | {:error, term()}
defmacro __using__(opts) do
policy_type = Keyword.fetch!(opts, :policy_type)
@@ -30,7 +30,7 @@ defmodule PolicyService.Aggregates.PolicyApplication do
ProviderQuoteReceived,
AllQuotesReceived,
QuoteAccepted,
SolicitationSent,
SolicitationRequestSent,
PolicyIssued
}
@@ -39,29 +39,29 @@ defmodule PolicyService.Aggregates.PolicyApplication do
defstruct [
:id,
:submitted_by,
:applicant_info,
:policy_details,
:insured,
:buyer,
:insured_object,
:selected_providers,
:accepted_quote_id,
:accepted_plan_id,
:accepted_provider_id,
:solicitation_id,
:policy_number,
:accepted_by,
:provider_policy_number,
:effective_date,
:expiry_date,
:state,
quotes: %{},
pending_endorsements: %{}
endorsements: %{}
]
# ── Execute ────────────────────────────────────────────────────────────
# ── Execute ──────────────────────────────────────────────────
@impl Commanded.Aggregates.Aggregate
def execute(%__MODULE__{state: nil}, %SubmitPolicyApplication{} = cmd) do
with :ok <- PolicyService.Aggregates.PolicyApplication.validate_policy_id(cmd.id),
:ok <- validate_insured(cmd.insured),
:ok <-
PolicyService.Aggregates.PolicyApplication.validate_applicant(cmd.applicant_info),
:ok <- validate_details(cmd.policy_details),
PolicyService.Aggregates.PolicyApplication.validate_buyer(cmd.buyer),
:ok <- validate_insured_object(cmd.insured_object),
:ok <-
PolicyService.Aggregates.PolicyApplication.validate_providers(
cmd.selected_providers
@@ -72,8 +72,9 @@ defmodule PolicyService.Aggregates.PolicyApplication do
id: cmd.id,
provider_id: provider.provider_id,
provider_email: provider.email,
applicant_info: cmd.applicant_info,
policy_details: cmd.policy_details,
insured: cmd.insured,
buyer: cmd.buyer,
insured_object: cmd.insured_object,
requested_at: DateTime.utc_now()
}
end)
@@ -82,8 +83,9 @@ defmodule PolicyService.Aggregates.PolicyApplication do
%PolicyApplicationSubmitted{
id: cmd.id,
submitted_by: cmd.submitted_by,
applicant_info: cmd.applicant_info,
policy_details: cmd.policy_details,
insured: cmd.insured,
buyer: cmd.buyer,
insured_object: cmd.insured_object,
selected_providers: cmd.selected_providers,
submitted_at: DateTime.utc_now()
}
@@ -140,19 +142,30 @@ defmodule PolicyService.Aggregates.PolicyApplication do
end
def execute(%__MODULE__{} = agg, %AcceptQuoteAndSolicit{} = cmd) do
with {:ok, quote} <-
PolicyService.Aggregates.PolicyApplication.find_quote(agg, cmd.quote_id),
{:ok, provider} <-
PolicyService.Aggregates.PolicyApplication.find_provider(agg, quote.provider_id),
{:ok, plan} <-
PolicyService.Aggregates.PolicyApplication.find_plan(quote, cmd.plan_id) do
case Enum.find_value(agg.quotes, fn {provider_id, quote} ->
case Enum.find(quote.plans, &(&1.plan_id == cmd.accepted_plan_id)) do
nil -> nil
plan -> %{quote: quote, provider: provider_id, plan: plan}
end
end) do
nil ->
{:error, :plan_not_found}
result ->
[
%QuoteAccepted{
id: agg.id,
quote: quote,
plan: plan,
provider: provider,
accepted_at: DateTime.utc_now()
quote: result.quote,
plan: result.plan,
provider: result.provider,
accepted_by: cmd.accepted_by
},
%SolicitationRequestSent{
id: agg.id,
plan: result.plan,
provider_id: result.provider
}
]
end
end
@@ -162,14 +175,14 @@ defmodule PolicyService.Aggregates.PolicyApplication do
def execute(%__MODULE__{} = agg, %RecordPolicyIssued{} = cmd) do
%PolicyIssued{
id: agg.id,
policy_number: cmd.policy_number,
provider_policy_number: cmd.provider_policy_number,
effective_date: cmd.effective_date,
expiry_date: cmd.expiry_date,
issued_at: cmd.issued_at || DateTime.utc_now()
}
end
# ── Apply ──────────────────────────────────────────────────────────────
# ── Apply ──────────────────────────────────────────────────────
@impl Commanded.Aggregates.Aggregate
def apply(%__MODULE__{} = agg, %PolicyApplicationSubmitted{} = e) do
@@ -177,8 +190,9 @@ defmodule PolicyService.Aggregates.PolicyApplication do
agg
| id: e.id,
submitted_by: e.submitted_by,
applicant_info: e.applicant_info,
policy_details: e.policy_details,
insured: e.insured,
buyer: e.buyer,
insured_object: e.insured_object,
selected_providers: e.selected_providers,
quotes: %{},
state: :awaiting_quotes
@@ -205,44 +219,41 @@ defmodule PolicyService.Aggregates.PolicyApplication do
def apply(%__MODULE__{} = agg, %QuoteAccepted{} = e) do
%__MODULE__{
agg
| accepted_quote_id: e.quote.quote_id,
accepted_plan_id: e.plan.plan_id,
accepted_provider_id: e.provider.provider_id,
state: :solicitation_sent
| accepted_plan_id: e.plan.plan_id,
accepted_by: e.accepted_by
}
end
def apply(%__MODULE__{} = agg, %SolicitationSent{} = e) do
%__MODULE__{agg | solicitation_id: e.solicitation_id}
def apply(%__MODULE__{} = agg, %SolicitationRequestSent{} = _e) do
%__MODULE__{
agg
| state: :awaiting_policy
}
end
def apply(%__MODULE__{} = agg, %PolicyIssued{} = e) do
%__MODULE__{
agg
| policy_number: e.policy_number,
| provider_policy_number: e.provider_policy_number,
effective_date: e.effective_date,
expiry_date: e.expiry_date,
state: :issued
}
end
# allow each aggregate to override any callback
defoverridable execute: 2, apply: 2
end
end
# ── Validation ───────────────────────────────────────────────────
def validate_policy_id(%PolicyService.Aggregates.PolicyId{policy_type: _}), do: :ok
def validate_policy_id(_), do: {:error, :invalid_policy_id_format}
def validate_user(id) when is_binary(id) and byte_size(id) > 0, do: :ok
def validate_user(_), do: {:error, :missing_user_id}
def validate_applicant(%{"name" => n, "date_of_birth" => _, "document_id" => d})
def validate_insured(%{
"type" => "individual",
"name" => n,
"date_of_birth" => _,
"document_id" => d
})
when is_binary(n) and is_binary(d) and byte_size(n) > 0 and byte_size(d) > 0,
do: :ok
# Match on string keys for Company
def validate_applicant(%{
def validate_insured(%{
"type" => "corporate",
"company_name" => c,
"ruc" => r,
"legal_rep_name" => rep,
@@ -252,7 +263,40 @@ defmodule PolicyService.Aggregates.PolicyApplication do
byte_size(c) > 0 and byte_size(r) > 0,
do: :ok
def validate_applicant(_), do: {:error, :invalid_applicant_info}
def validate_insured(_), do: {:error, :invalid_insured_info}
# allow each aggregate to override any callback
defoverridable execute: 2, apply: 2, validate_insured: 1
end
end
def validate_policy_id(%PolicyService.Aggregates.PolicyId{policy_type: _}), do: :ok
def validate_policy_id(_), do: {:error, :invalid_policy_id_format}
def validate_user(id) when is_binary(id) and byte_size(id) > 0, do: :ok
def validate_user(_), do: {:error, :missing_user_id}
def validate_buyer(%{
"type" => "individual",
"name" => n,
"date_of_birth" => _,
"document_id" => d
})
when is_binary(n) and is_binary(d) and byte_size(n) > 0 and byte_size(d) > 0,
do: :ok
def validate_buyer(%{
"type" => "corporate",
"company_name" => c,
"ruc" => r,
"legal_rep_name" => rep,
"legal_rep_document" => rd
})
when is_binary(c) and is_binary(r) and is_binary(rep) and is_binary(rd) and
byte_size(c) > 0 and byte_size(r) > 0,
do: :ok
def validate_buyer(_), do: {:error, :invalid_buyer_info}
def validate_providers(p) when is_list(p) and length(p) > 0, do: :ok
def validate_providers(_), do: {:error, :no_providers_selected}

View File

@@ -1,4 +1,9 @@
defmodule PolicyService.Aggregates.PolicyId do
@type t :: %__MODULE__{
org_id: String.t(),
policy_type: String.t(),
application_id: String.t()
}
@derive Jason.Encoder
defstruct [:org_id, :policy_type, :application_id]
@@ -43,6 +48,10 @@ defmodule PolicyService.Aggregates.PolicyId do
end
defimpl Commanded.Serialization.JsonDecoder do
def decode(%{org_id: org_id, policy_type: policy_type, application_id: application_id}) do
PolicyService.Aggregates.PolicyId.new(org_id, policy_type, application_id)
end
def decode(id), do: id
end
end

View File

@@ -10,14 +10,19 @@ defmodule PolicyService.Application do
children = [
PolicyService.CommandedApp,
PolicyService.Handlers.QuoteRequestHandler,
PolicyService.Consumers.QuoteReceivedConsumer,
PolicyService.Projectors.PolicyProjector,
PolicyService.Consumers.PolicyIssuedConsumer,
PolicyService.Handlers.SolicitationRequestHandler,
PolicyServiceWeb.Telemetry,
PolicyService.Consumers.QuoteTaskConsumer,
PolicyService.Consumers.SolicitationTaskConsumer,
PolicyService.Projectors.PolicyProjector,
PolicyService.Repo,
{DNSCluster, query: Application.get_env(:policy_service, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: PolicyService.PubSub},
{Phoenix.PubSub, name: PolicyService.PubSub, pool_size: 1},
PolicyServiceWeb.Telemetry,
{Oidcc.ProviderConfiguration.Worker,
%{
issuer: Application.get_env(:policy_service, :zitadel)[:issuer],
name: PolicyService.ZitadelProvider
}},
PolicyServiceWeb.Endpoint
]

View File

@@ -13,15 +13,39 @@ defmodule PolicyService.Router do
identity: :id
)
# Route Fire commands to Fire Aggregate
# Route Life commands to Life Aggregate
dispatch(
[
PolicyService.Commands.FirePolicy.SubmitPolicyApplication,
PolicyService.Commands.FirePolicy.RecordProviderQuote,
PolicyService.Commands.FirePolicy.AcceptQuoteAndSolicit,
PolicyService.Commands.FirePolicy.RecordPolicyIssued
PolicyService.Commands.LifePolicy.SubmitPolicyApplication,
PolicyService.Commands.LifePolicy.RecordProviderQuote,
PolicyService.Commands.LifePolicy.AcceptQuoteAndSolicit,
PolicyService.Commands.LifePolicy.RecordPolicyIssued
],
to: PolicyService.Aggregates.FirePolicyApplication,
to: PolicyService.Aggregates.LifePolicyApplication,
identity: :id
)
# Route FireStructure commands to FireStructure Aggregate
dispatch(
[
PolicyService.Commands.FireStructurePolicy.SubmitPolicyApplication,
PolicyService.Commands.FireStructurePolicy.RecordProviderQuote,
PolicyService.Commands.FireStructurePolicy.AcceptQuoteAndSolicit,
PolicyService.Commands.FireStructurePolicy.RecordPolicyIssued
],
to: PolicyService.Aggregates.FireStructurePolicyApplication,
identity: :id
)
# Route FireContents commands to FireContents Aggregate
dispatch(
[
PolicyService.Commands.FireContentsPolicy.SubmitPolicyApplication,
PolicyService.Commands.FireContentsPolicy.RecordProviderQuote,
PolicyService.Commands.FireContentsPolicy.AcceptQuoteAndSolicit,
PolicyService.Commands.FireContentsPolicy.RecordPolicyIssued
],
to: PolicyService.Aggregates.FireContentsPolicyApplication,
identity: :id
)
end

View File

@@ -1,8 +1,8 @@
defmodule PolicyService.Commands.CarPolicy do
alias PolicyService.Commands.Policy
defmodule SubmitPolicyApplication,
do: use(PolicyService.Commands.Policy.SubmitPolicyApplication)
defmodule SubmitPolicyApplication, do: use(Policy.SubmitPolicyApplication)
defmodule RecordProviderQuote, do: use(Policy.RecordProviderQuote)
defmodule AcceptQuoteAndSolicit, do: use(Policy.AcceptQuoteAndSolicit)
defmodule RecordPolicyIssued, do: use(Policy.RecordPolicyIssued)
defmodule RecordProviderQuote, do: use(PolicyService.Commands.Policy.RecordProviderQuote)
defmodule AcceptQuoteAndSolicit, do: use(PolicyService.Commands.Policy.AcceptQuoteAndSolicit)
defmodule RecordPolicyIssued, do: use(PolicyService.Commands.Policy.RecordPolicyIssued)
end

View File

@@ -0,0 +1,8 @@
defmodule PolicyService.Commands.FireContentsPolicy do
defmodule SubmitPolicyApplication,
do: use(PolicyService.Commands.Policy.SubmitPolicyApplication)
defmodule RecordProviderQuote, do: use(PolicyService.Commands.Policy.RecordProviderQuote)
defmodule AcceptQuoteAndSolicit, do: use(PolicyService.Commands.Policy.AcceptQuoteAndSolicit)
defmodule RecordPolicyIssued, do: use(PolicyService.Commands.Policy.RecordPolicyIssued)
end

View File

@@ -1,8 +0,0 @@
defmodule PolicyService.Commands.FirePolicy do
alias PolicyService.Commands.Policy
defmodule SubmitPolicyApplication, do: use(Policy.SubmitPolicyApplication)
defmodule RecordProviderQuote, do: use(Policy.RecordProviderQuote)
defmodule AcceptQuoteAndSolicit, do: use(Policy.AcceptQuoteAndSolicit)
defmodule RecordPolicyIssued, do: use(Policy.RecordPolicyIssued)
end

View File

@@ -0,0 +1,8 @@
defmodule PolicyService.Commands.FireStructurePolicy do
defmodule SubmitPolicyApplication,
do: use(PolicyService.Commands.Policy.SubmitPolicyApplication)
defmodule RecordProviderQuote, do: use(PolicyService.Commands.Policy.RecordProviderQuote)
defmodule AcceptQuoteAndSolicit, do: use(PolicyService.Commands.Policy.AcceptQuoteAndSolicit)
defmodule RecordPolicyIssued, do: use(PolicyService.Commands.Policy.RecordPolicyIssued)
end

View File

@@ -0,0 +1,8 @@
defmodule PolicyService.Commands.LifePolicy do
defmodule SubmitPolicyApplication,
do: use(PolicyService.Commands.Policy.SubmitPolicyApplication)
defmodule RecordProviderQuote, do: use(PolicyService.Commands.Policy.RecordProviderQuote)
defmodule AcceptQuoteAndSolicit, do: use(PolicyService.Commands.Policy.AcceptQuoteAndSolicit)
defmodule RecordPolicyIssued, do: use(PolicyService.Commands.Policy.RecordPolicyIssued)
end

View File

@@ -1,19 +1,21 @@
defmodule PolicyService.Commands.Policy do
@moduledoc """
Base templates for Policy commands.
Use these macros to ensure all policy types share the same structure.
Base templates for Policy commands using TypedStruct.
"""
defmodule SubmitPolicyApplication do
defmacro __using__(_opts) do
quote do
defstruct [
:id,
:submitted_by,
:applicant_info,
:policy_details,
:selected_providers
]
use TypedStruct
typedstruct do
field :id, PolicyService.Aggregates.PolicyId.t(), enforce: true
field :submitted_by, String.t(), enforce: true
field :insured, map(), enforce: true
field :buyer, map(), enforce: true
field :insured_object, map()
field :selected_providers, list(), enforce: true
end
end
end
end
@@ -21,16 +23,18 @@ defmodule PolicyService.Commands.Policy do
defmodule RecordProviderQuote do
defmacro __using__(_opts) do
quote do
defstruct [
:id,
:recorded_by,
:provider_id,
:quote_id,
:premium,
:coverage_details,
:valid_until,
:plans
]
use TypedStruct
typedstruct do
field :id, PolicyService.Aggregates.PolicyId.t(), enforce: true
field :recorded_by, String.t(), enforce: true
field :provider_id, String.t(), enforce: true
field :quote_id, String.t(), enforce: true
field :premium, String.t()
field :coverage_details, map()
field :valid_until, String.t(), enforce: true
field :plans, list(), enforce: true
end
end
end
end
@@ -38,13 +42,13 @@ defmodule PolicyService.Commands.Policy do
defmodule AcceptQuoteAndSolicit do
defmacro __using__(_opts) do
quote do
defstruct [
:id,
:accepted_by,
:quote_id,
:plan_id,
:solicitation_fields
]
use TypedStruct
typedstruct do
field :id, PolicyService.Aggregates.PolicyId.t(), enforce: true
field :accepted_by, String.t()
field :accepted_plan_id, String.t(), enforce: true
end
end
end
end
@@ -52,13 +56,15 @@ defmodule PolicyService.Commands.Policy do
defmodule RecordPolicyIssued do
defmacro __using__(_opts) do
quote do
defstruct [
:id,
:policy_number,
:effective_date,
:expiry_date,
:issued_at
]
use TypedStruct
typedstruct do
field :id, PolicyService.Aggregates.PolicyId.t(), enforce: true
field :provider_policy_number, String.t(), enforce: true
field :effective_date, String.t(), enforce: true
field :expiry_date, String.t(), enforce: true
field :issued_at, String.t()
end
end
end
end

View File

@@ -1,73 +0,0 @@
defmodule PolicyService.Consumers.PolicyIssuedConsumer do
use GenServer
require Logger
alias PolicyService.CommandedApp
alias PolicyService.Commands.CarPolicy
alias PolicyService.Aggregates.PolicyId
@exchange "carrier_inbox.events"
@queue "policy_service.policy_issued"
@routing_key "policy.issued"
def start_link(_opts), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
def init(_) do
{:ok, conn} = AMQP.Connection.open(amqp_url())
{:ok, channel} = AMQP.Channel.open(conn)
AMQP.Exchange.topic(channel, @exchange, durable: true)
AMQP.Queue.declare(channel, @queue, durable: true)
AMQP.Queue.bind(channel, @queue, @exchange, routing_key: @routing_key)
AMQP.Basic.qos(channel, prefetch_count: 10)
{:ok, _tag} = AMQP.Basic.consume(channel, @queue)
{:ok, %{channel: channel}}
end
def handle_info({:basic_consume_ok, _}, state), do: {:noreply, state}
def handle_info({:basic_cancel, _}, state), do: {:stop, :normal, state}
def handle_info({:basic_cancel_ok, _}, state), do: {:noreply, state}
def handle_info({:basic_deliver, payload, meta}, state) do
case Jason.decode(payload) do
{:ok, event} ->
process(event, meta, state)
{:error, _} ->
Logger.error("PolicyIssuedConsumer: failed to decode payload")
AMQP.Basic.reject(state.channel, meta.delivery_tag, requeue: false)
end
{:noreply, state}
end
defp process(event, meta, state) do
%{policy_type: policy_type} = PolicyId.parse!(event["id"])
command =
case policy_type do
"car" ->
%CarPolicy.RecordPolicyIssued{
id: event["id"],
policy_number: event["policy_number"],
effective_date: event["effective_date"],
expiry_date: event["expiry_date"],
issued_at: DateTime.utc_now()
}
end
case CommandedApp.dispatch(command) do
:ok ->
AMQP.Basic.ack(state.channel, meta.delivery_tag)
{:error, reason} ->
Logger.error("PolicyIssuedConsumer: dispatch failed: #{inspect(reason)}")
AMQP.Basic.reject(state.channel, meta.delivery_tag, requeue: true)
end
end
defp amqp_url do
Application.get_env(:policy_service, :amqp_url, "amqp://guest:guest@localhost:5672")
end
end

View File

@@ -1,120 +0,0 @@
defmodule PolicyService.Consumers.QuoteReceivedConsumer do
use GenServer
require Logger
alias PolicyService.CommandedApp
alias PolicyService.Commands.CarPolicy
alias PolicyService.Aggregates.PolicyId
@exchange "carrier_inbox.events"
@queue "policy_service.quote_received"
@routing_key "quote.received"
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
amqp_url = Application.fetch_env!(:policy_service, :amqp_url)
{:ok, conn} = AMQP.Connection.open(amqp_url)
{:ok, channel} = AMQP.Channel.open(conn)
AMQP.Exchange.declare(channel, @exchange, :topic, durable: true)
AMQP.Queue.declare(channel, @queue, durable: true)
AMQP.Queue.bind(channel, @queue, @exchange, routing_key: @routing_key)
AMQP.Basic.consume(channel, @queue, nil, no_ack: false)
Logger.info("QuoteReceivedConsumer started, listening on #{@queue}")
{:ok, %{conn: conn, channel: channel}}
end
# ---------------------------------------------------------------------------
# AMQP callbacks
# ---------------------------------------------------------------------------
def handle_info({:basic_consume_ok, _}, state), do: {:noreply, state}
def handle_info({:basic_cancel, _}, state), do: {:stop, :normal, state}
def handle_info({:basic_cancel_ok, _}, state), do: {:noreply, state}
def handle_info({:basic_deliver, payload, %{delivery_tag: tag}}, state) do
case process(payload) do
:ok ->
AMQP.Basic.ack(state.channel, tag)
{:error, reason} ->
Logger.error("Failed to process quote.received: #{inspect(reason)}")
AMQP.Basic.nack(state.channel, tag, requeue: false)
end
{:noreply, state}
end
# ---------------------------------------------------------------------------
# Processing
# ---------------------------------------------------------------------------
defp process(payload) do
with {:ok, event} <- Jason.decode(payload),
{:ok, cmd} <- build_command(event),
:ok <- CommandedApp.dispatch(cmd, consistency: :strong) do
:ok
end
end
defp build_command(event) do
case event["policy_type"] do
"car" -> build_car_command(event)
type -> {:error, {:unsupported_policy_type, type}}
end
end
defp build_car_command(event) do
%{policy_type: policy_type} = PolicyId.parse!(event["id"])
case policy_type do
"car" ->
cmd = %CarPolicy.RecordProviderQuote{
id: PolicyId.parse!(event["id"]),
recorded_by: event["entered_by"],
provider_id: event["provider_id"],
quote_id: event["quote_id"],
valid_until: parse_date(event["valid_until"]),
plans: parse_plans(event["plans"])
}
{:ok, cmd}
end
rescue
e -> {:error, e}
end
defp parse_plans(nil), do: []
defp parse_plans(plans) when is_list(plans) do
Enum.map(plans, fn p ->
%{
plan_id: p["plan_id"],
name: p["name"],
premium: p["premium"],
coverage_details: p["coverage_details"],
deductible: p["deductible"],
coverage_limit: p["coverage_limit"]
}
end)
end
defp parse_date(nil), do: nil
defp parse_date(%Date{} = d), do: d
defp parse_date(s) when is_binary(s) do
case Date.from_iso8601(s) do
{:ok, d} -> d
_ -> nil
end
end
end

View File

@@ -0,0 +1,154 @@
defmodule PolicyService.Consumers.QuoteTaskConsumer do
use GenServer
require Logger
alias PolicyService.CommandedApp
alias PolicyService.Commands.{CarPolicy, LifePolicy, FireStructurePolicy, FireContentsPolicy}
alias PolicyService.Aggregates.PolicyId
@exchange "workload_service.events.quote_task_completed"
@queue "policy_service.quote_task_completed"
@routing_key "quote_task.completed"
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
{:ok, conn} = AMQP.Connection.open(Application.fetch_env!(:policy_service, :amqp_url))
{:ok, channel} = AMQP.Channel.open(conn)
AMQP.Queue.declare(channel, @queue, durable: true)
AMQP.Queue.bind(channel, @queue, @exchange, routing_key: @routing_key)
{:ok, _tag} = AMQP.Basic.consume(channel, @queue)
Logger.info("QuoteTaskConsumer started, listening on #{@queue}")
{:ok, %{channel: channel}}
end
def handle_info({:basic_deliver, payload, meta}, state) do
:ok =
case process(payload) do
:ok ->
AMQP.Basic.ack(state.channel, meta.delivery_tag)
{:error, reason} ->
Logger.error("QuoteTaskConsumer: failed to process: #{inspect(reason)}")
AMQP.Basic.reject(state.channel, meta.delivery_tag, requeue: false)
end
{:noreply, state}
end
def handle_info({:basic_consume_ok, _}, state), do: {:noreply, state}
def handle_info({:basic_cancel, _}, state), do: {:stop, :normal, state}
def handle_info({:basic_cancel_ok, _}, state), do: {:noreply, state}
defp process(payload) do
with {:ok, event} <- Jason.decode(payload),
:ok <- dispatch(event) do
:ok
end
end
defp dispatch(%{
"application_id" => %{
"org_id" => org_id,
"policy_type" => policy_type,
"application_id" => app_id
},
"task_info" => %{"provider_id" => provider_id},
"submission" => %{
"quote_id" => quote_id,
"recorded_by" => recorded_by,
"valid_until" => valid_until,
"plans" => plans
}
}) do
cmd =
case policy_type do
"car" ->
%CarPolicy.RecordProviderQuote{
id: PolicyId.new(org_id, policy_type, app_id),
recorded_by: recorded_by || "system",
provider_id: provider_id,
quote_id: quote_id,
valid_until: parse_date(valid_until),
plans:
Enum.map(
plans || [],
&%{
plan_id: &1["plan_id"],
name: &1["name"],
premium: &1["premium"],
coverage_details: &1["coverage_details"]
}
)
}
"life" ->
%LifePolicy.RecordProviderQuote{
id: PolicyId.new(org_id, policy_type, app_id),
recorded_by: recorded_by || "system",
provider_id: provider_id,
quote_id: quote_id,
valid_until: parse_date(valid_until),
plans:
Enum.map(
plans || [],
&%{
plan_id: &1["plan_id"],
name: &1["name"],
premium: &1["premium"],
coverage_details: &1["coverage_details"]
}
)
}
"fire_structure" ->
%FireStructurePolicy.RecordProviderQuote{
id: PolicyId.new(org_id, policy_type, app_id),
recorded_by: recorded_by || "system",
provider_id: provider_id,
quote_id: quote_id,
valid_until: parse_date(valid_until),
plans:
Enum.map(
plans || [],
&%{
plan_id: &1["plan_id"],
name: &1["name"],
premium: &1["premium"],
coverage_details: &1["coverage_details"]
}
)
}
"fire_contents" ->
%FireContentsPolicy.RecordProviderQuote{
id: PolicyId.new(org_id, policy_type, app_id),
recorded_by: recorded_by || "system",
provider_id: provider_id,
quote_id: quote_id,
valid_until: parse_date(valid_until),
plans:
Enum.map(
plans || [],
&%{
plan_id: &1["plan_id"],
name: &1["name"],
premium: &1["premium"],
coverage_details: &1["coverage_details"]
}
)
}
end
CommandedApp.dispatch(cmd, consistency: :strong)
end
defp parse_date(nil), do: nil
defp parse_date(s) when is_binary(s), do: Date.from_iso8601(s) |> elem(1)
end

View File

@@ -0,0 +1,117 @@
defmodule PolicyService.Consumers.SolicitationTaskConsumer do
use GenServer
require Logger
alias PolicyService.CommandedApp
alias PolicyService.Commands.{CarPolicy, LifePolicy, FireStructurePolicy, FireContentsPolicy}
alias PolicyService.Aggregates.PolicyId
@exchange "workload_service.events.solicitation_task_completed"
@queue "policy_service.solicitation_task_completed"
@routing_key "solicitation_task.completed"
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(_opts) do
{:ok, conn} = AMQP.Connection.open(Application.fetch_env!(:policy_service, :amqp_url))
{:ok, channel} = AMQP.Channel.open(conn)
AMQP.Queue.declare(channel, @queue, durable: true)
AMQP.Queue.bind(channel, @queue, @exchange, routing_key: @routing_key)
{:ok, _tag} = AMQP.Basic.consume(channel, @queue)
Logger.info("SolicitationTaskConsumer started, listening on #{@queue}")
{:ok, %{channel: channel}}
end
def handle_info({:basic_deliver, payload, meta}, state) do
:ok =
case process(payload) do
:ok ->
AMQP.Basic.ack(state.channel, meta.delivery_tag)
{:error, reason} ->
Logger.error("SolicitationTaskConsumer: failed to process: #{inspect(reason)}")
AMQP.Basic.reject(state.channel, meta.delivery_tag, requeue: false)
end
{:noreply, state}
end
def handle_info({:basic_consume_ok, _}, state), do: {:noreply, state}
def handle_info({:basic_cancel, _}, state), do: {:stop, :normal, state}
def handle_info({:basic_cancel_ok, _}, state), do: {:noreply, state}
defp process(payload) do
with {:ok, event} <- Jason.decode(payload),
:ok <- dispatch(event) do
:ok
end
end
defp dispatch(%{
"application_id" => %{
"org_id" => org_id,
"policy_type" => policy_type,
"application_id" => app_id
},
"task_info" => _task_info,
"submission" => submission
}) do
cmd =
case policy_type do
"car" ->
%CarPolicy.RecordPolicyIssued{
id: PolicyId.new(org_id, policy_type, app_id),
provider_policy_number: Map.get(submission, "provider_policy_number"),
effective_date: Map.get(submission, "effective_date"),
expiry_date: Map.get(submission, "expiry_date"),
issued_at:
Map.get(submission, "issued_at") || DateTime.utc_now() |> DateTime.to_iso8601()
}
"life" ->
%LifePolicy.RecordPolicyIssued{
id: PolicyId.new(org_id, policy_type, app_id),
provider_policy_number: Map.get(submission, "provider_policy_number"),
effective_date: Map.get(submission, "effective_date"),
expiry_date: Map.get(submission, "expiry_date"),
issued_at:
Map.get(submission, "issued_at") || DateTime.utc_now() |> DateTime.to_iso8601()
}
"fire_structure" ->
%FireStructurePolicy.RecordPolicyIssued{
id: PolicyId.new(org_id, policy_type, app_id),
provider_policy_number: Map.get(submission, "provider_policy_number"),
effective_date: Map.get(submission, "effective_date"),
expiry_date: Map.get(submission, "expiry_date"),
issued_at:
Map.get(submission, "issued_at") || DateTime.utc_now() |> DateTime.to_iso8601()
}
"fire_contents" ->
%FireContentsPolicy.RecordPolicyIssued{
id: PolicyId.new(org_id, policy_type, app_id),
provider_policy_number: Map.get(submission, "provider_policy_number"),
effective_date: Map.get(submission, "effective_date"),
expiry_date: Map.get(submission, "expiry_date"),
issued_at:
Map.get(submission, "issued_at") || DateTime.utc_now() |> DateTime.to_iso8601()
}
end
case CommandedApp.dispatch(cmd, consistency: :strong) do
:ok ->
Logger.info("SolicitationTaskConsumer: issued policy for #{app_id}")
:ok
{:error, reason} ->
Logger.error("SolicitationTaskConsumer: failed to issue policy - #{inspect(reason)}")
{:error, reason}
end
end
end

View File

@@ -1,29 +1,61 @@
defmodule PolicyService.Events do
@moduledoc """
Events macro for adding JsonDecoder to domain events.
"""
alias PolicyService.Aggregates.PolicyId
defmacro __using__(_opts) do
quote do
defimpl Commanded.Serialization.JsonDecoder do
def decode(
%{id: %{org_id: org_id, policy_type: policy_type, application_id: application_id}} =
event
) do
%{event | id: PolicyId.new(org_id, policy_type, application_id)}
end
def decode(event), do: event
end
end
end
end
defmodule PolicyService.Events.Policy do
@moduledoc """
Policy domain events.
"""
defmodule PolicyApplicationSubmitted do
use PolicyService.Events
@derive Jason.Encoder
defstruct [
:id,
:submitted_by,
:applicant_info,
:policy_details,
:insured,
:buyer,
:insured_object,
:selected_providers,
:submitted_at
]
end
defmodule QuoteRequestSent do
use PolicyService.Events
@derive Jason.Encoder
defstruct [
:id,
:provider_id,
:provider_email,
:applicant_info,
:policy_details,
:insured,
:buyer,
:insured_object,
:requested_at
]
end
defmodule ProviderQuoteReceived do
use PolicyService.Events
@derive Jason.Encoder
defstruct [
:id,
@@ -39,42 +71,26 @@ defmodule PolicyService.Events.Policy do
end
defmodule AllQuotesReceived do
use PolicyService.Events
@derive Jason.Encoder
defstruct [:id, :org_id, :quote_count]
defstruct [:id, :quote_count]
end
defmodule QuoteAccepted do
use PolicyService.Events
@derive Jason.Encoder
defstruct [
:id,
:accepted_by,
:quote,
:plan,
:provider,
:accepted_at
]
defstruct [:id, :accepted_by, :quote, :plan, :provider]
end
defmodule SolicitationSent do
defmodule SolicitationRequestSent do
use PolicyService.Events
@derive Jason.Encoder
defstruct [
:id,
:solicitation_id,
:provider_id,
:template_id,
:s3_key,
:sent_at
]
defstruct [:id, :plan, :provider_id]
end
defmodule PolicyIssued do
use PolicyService.Events
@derive Jason.Encoder
defstruct [
:id,
:policy_number,
:effective_date,
:expiry_date,
:issued_at
]
defstruct [:id, :provider_policy_number, :effective_date, :expiry_date, :issued_at]
end
end

View File

@@ -6,6 +6,10 @@ defmodule PolicyService.Handlers.QuoteRequestHandler do
alias PolicyService.Events.Policy.QuoteRequestSent
def handle(%QuoteRequestSent{} = e, _metadata) do
PolicyService.MessageBus.publish("quote.requested", e)
PolicyService.MessageBus.publish(
"policy_service.events.quote_requested",
"quote.requested",
e
)
end
end

View File

@@ -5,11 +5,16 @@ defmodule PolicyService.Handlers.SolicitationRequestHandler do
require Logger
alias PolicyService.Events.Policy.QuoteAccepted
alias PolicyService.Events.Policy.SolicitationRequestSent
alias PolicyService.MessageBus
def handle(%QuoteAccepted{} = event, _metadata) do
MessageBus.publish("quote.accepted", event)
def handle(%SolicitationRequestSent{} = event, _metadata) do
MessageBus.publish(
"policy_service.events.solicitation_requested",
"solicitation.requested",
event
)
:ok
end
end

View File

@@ -1,11 +1,11 @@
defmodule PolicyService.MessageBus do
use AMQP
def publish(routing_key, event) do
def publish(exchange, routing_key, event) do
payload = Jason.encode!(event)
:ok =
AMQP.Basic.publish(channel(), "policy_service.events", routing_key, payload,
AMQP.Basic.publish(channel(), exchange, routing_key, payload,
content_type: "application/json",
persistent: true
)

View File

@@ -7,11 +7,15 @@ defmodule PolicyService.Filters.PolicyApplicationFilters do
where(
query,
[p],
fragment("?->>'name' ilike ?", p.applicant_info, ^term) or
fragment("?->>'company_name' ilike ?", p.applicant_info, ^term) or
fragment("?->>'document_id' ilike ?", p.applicant_info, ^term) or
fragment("?->>'ruc' ilike ?", p.applicant_info, ^term) or
ilike(p.policy_number, ^term)
fragment("?->>'name' ilike ?", p.insured, ^term) or
fragment("?->>'company_name' ilike ?", p.insured, ^term) or
fragment("?->>'document_id' ilike ?", p.insured, ^term) or
fragment("?->>'ruc' ilike ?", p.insured, ^term) or
fragment("?->>'name' ilike ?", p.buyer, ^term) or
fragment("?->>'company_name' ilike ?", p.buyer, ^term) or
fragment("?->>'document_id' ilike ?", p.buyer, ^term) or
fragment("?->>'ruc' ilike ?", p.buyer, ^term) or
ilike(p.provider_policy_number, ^term)
)
end
end

View File

@@ -8,18 +8,14 @@ defmodule PolicyService.Projections.PolicyApplication do
:org_id,
:submitted_by,
:policy_type,
:applicant_info,
:policy_details,
:insured,
:buyer,
:insured_object,
:selected_providers,
:quotes,
:accepted_quote_id,
:accepted_plan_id,
:accepted_provider_id,
:accepted_by,
:accepted_at,
:solicitation_id,
:solicitation_s3_key,
:policy_number,
:provider_policy_number,
:premium,
:effective_date,
:expiry_date,
@@ -39,7 +35,7 @@ defmodule PolicyService.Projections.PolicyApplication do
max_limit: 100,
custom_fields: [
search: [
filter: {PolicyService.Projections.PolicyApplicationFilters, :search, []},
filter: {PolicyService.Filters.PolicyApplicationFilters, :search, []},
ecto_type: :string,
operators: [:==]
]
@@ -55,22 +51,18 @@ defmodule PolicyService.Projections.PolicyApplication do
field :submitted_by, :string
field :policy_type, :string
field :applicant_info, :map
field :policy_details, :map
field :insured, :map
field :buyer, :map
field :insured_object, :map
field :selected_providers, {:array, :string}, default: []
field :quotes, :map, default: %{}
field :accepted_quote_id, :string
field :accepted_plan_id, :string
field :accepted_provider_id, :string
field :accepted_by, :string
field :accepted_at, :utc_datetime_usec
field :solicitation_id, :string
field :solicitation_s3_key, :string
field :provider_policy_number, :string
field :policy_number, :string
field :premium, :decimal
field :effective_date, :date
field :expiry_date, :date

View File

@@ -10,25 +10,23 @@ defmodule PolicyService.Projectors.PolicyProjector do
ProviderQuoteReceived,
AllQuotesReceived,
QuoteAccepted,
SolicitationSent,
SolicitationRequestSent,
PolicyIssued
}
alias PolicyService.Projections.PolicyApplication
alias PolicyService.Aggregates.PolicyId
import Ecto.Query
project(%PolicyApplicationSubmitted{} = e, _meta, fn multi ->
%{policy_type: policy_type, application_id: application_id, org_id: org_id} = e.id
Ecto.Multi.insert(multi, :policy_application, %PolicyApplication{
id: to_string(PolicyId.new(org_id, policy_type, application_id)),
application_id: application_id,
org_id: org_id,
id: to_string(e.id),
application_id: e.id.application_id,
org_id: e.id.org_id,
submitted_by: e.submitted_by,
policy_type: policy_type,
applicant_info: atomize(e.applicant_info),
policy_details: atomize(e.policy_details),
policy_type: e.id.policy_type,
insured: atomize(e.insured),
buyer: atomize(e.buyer),
insured_object: atomize(e.insured_object),
selected_providers: Enum.map(e.selected_providers, & &1["provider_id"]),
quotes: %{},
status: "quote_requested",
@@ -71,25 +69,20 @@ defmodule PolicyService.Projectors.PolicyProjector do
end)
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
Ecto.Changeset.change(p,
accepted_quote_id: e.quote.quote_id,
accepted_plan_id: e.plan.plan_id,
accepted_provider_id: e.provider.id,
accepted_at: parse_datetime(e.accepted_at),
status: "solicitation_sent"
accepted_by: e.accepted_by
)
end)
end)
project(%SolicitationSent{} = e, _meta, fn multi ->
project(%SolicitationRequestSent{} = e, _meta, fn multi ->
multi
|> Ecto.Multi.run(:fetch, fn repo, _ ->
{:ok, repo.get!(PolicyApplication, to_string(e.id))}
end)
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
Ecto.Changeset.change(p,
solicitation_id: e.solicitation_id,
solicitation_s3_key: e.s3_key,
solicitation_sent_at: parse_datetime(e.sent_at)
status: "awaiting_policy"
)
end)
end)
@@ -101,7 +94,7 @@ defmodule PolicyService.Projectors.PolicyProjector do
end)
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
Ecto.Changeset.change(p,
policy_number: e.policy_number,
provider_policy_number: e.provider_policy_number,
effective_date: parse_date(e.effective_date),
expiry_date: parse_date(e.expiry_date),
issued_at: parse_datetime(e.issued_at),

View File

@@ -0,0 +1,37 @@
defmodule PolicyService.Release do
@moduledoc """
Used for executing DB release tasks when run in production without Mix
installed.
"""
@app :policy_service
def migrate do
load_app()
init_event_store()
for repo <- repos() do
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
end
end
def rollback(repo, version) do
load_app()
{:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :down, to: version))
end
defp repos do
Application.fetch_env!(@app, :ecto_repos)
end
defp load_app do
Application.ensure_all_started(:ssl)
Application.ensure_all_started(:postgrex)
Application.ensure_loaded(@app)
end
def init_event_store do
config = PolicyService.EventStore.config()
:ok = EventStore.Tasks.Init.exec(config, [])
end
end

View File

@@ -1,5 +1,5 @@
defmodule PolicyServiceWeb.ApiSpec do
alias OpenApiSpex.{OpenApi, Info, Server}
alias OpenApiSpex.{OpenApi, Info, Server, Components, SecurityScheme}
alias OpenApiSpex.{Info, OpenApi, Paths, Server}
alias PolicyServiceWeb.{Endpoint, Router}
@behaviour OpenApi
@@ -16,7 +16,18 @@ defmodule PolicyServiceWeb.ApiSpec do
version: "1.0"
},
# Populate the paths from a phoenix router
paths: Paths.from_router(Router)
paths: Paths.from_router(Router),
components: %Components{
securitySchemes: %{
"bearerAuth" => %SecurityScheme{
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
description: "Zitadel JWT bearer token for authentication"
}
}
},
security: [%{"bearerAuth" => []}]
}
# Discover request/response schemas from path specs
|> OpenApiSpex.resolve_schema_modules()

View File

@@ -7,29 +7,23 @@ defmodule PolicyServiceWeb.PolicyController do
alias PolicyService.Aggregates.PolicyId
alias PolicyService.Commands.CarPolicy
alias PolicyService.Commands.LifePolicy
alias PolicyService.Commands.FireStructurePolicy
alias PolicyService.Commands.FireContentsPolicy
alias PolicyServiceWeb.Schemas.Policy, as: S
alias PolicyServiceWeb.QueryHelpers
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]
],
parameters:
QueryHelpers.flop(
[:status, :policy_type, :search],
[:submitted_at, :policy_type, :status]
),
responses: [
ok: {"Policy list", "application/json", S.PolicyListResponse},
bad_request: {"Invalid params", "application/json", S.ErrorResponse}
@@ -37,7 +31,7 @@ defmodule PolicyServiceWeb.PolicyController do
)
def index(conn, params) do
org_id = conn.assigns[:org_id] || "test"
org_id = conn.private[PolicyServiceWeb.Plugs.ExtractOrganizationId]
case PolicyQueries.list_by_org(org_id, params) do
{:ok, {policies, meta}} ->
@@ -69,7 +63,7 @@ defmodule PolicyServiceWeb.PolicyController do
)
def show(conn, %{"application_id" => application_id}) do
org_id = conn.assigns[:org_id] || "test"
org_id = conn.private[PolicyServiceWeb.Plugs.ExtractOrganizationId]
case PolicyQueries.get_by_application_id(org_id, application_id) do
{:ok, policy} ->
@@ -95,12 +89,13 @@ defmodule PolicyServiceWeb.PolicyController do
def create(conn, params) do
application_id = Ecto.UUID.generate()
org_id = conn.assigns[:org_id] || "test"
submitted_by = conn.assigns[:user_id] || "test"
org_id = conn.private[PolicyServiceWeb.Plugs.ExtractOrganizationId]
submitted_by = conn.assigns[:user_id]
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, insured} <- parse_insured(params["insured"]),
{:ok, buyer} <- parse_buyer(params["buyer"]),
{:ok, insured_object} <- parse_insured_object(policy_type, params["insured_object"]),
{:ok, providers} <- parse_providers(params["selected_providers"]) do
command =
case policy_type do
@@ -108,8 +103,39 @@ defmodule PolicyServiceWeb.PolicyController do
%CarPolicy.SubmitPolicyApplication{
id: PolicyId.new(org_id, policy_type, application_id),
submitted_by: submitted_by,
applicant_info: applicant_info,
policy_details: policy_details,
insured: insured,
buyer: buyer,
insured_object: insured_object,
selected_providers: providers
}
"life" ->
%LifePolicy.SubmitPolicyApplication{
id: PolicyId.new(org_id, policy_type, application_id),
submitted_by: submitted_by,
insured: insured,
buyer: buyer,
insured_object: insured_object,
selected_providers: providers
}
"fire_structure" ->
%FireStructurePolicy.SubmitPolicyApplication{
id: PolicyId.new(org_id, policy_type, application_id),
submitted_by: submitted_by,
insured: insured,
buyer: buyer,
insured_object: insured_object,
selected_providers: providers
}
"fire_contents" ->
%FireContentsPolicy.SubmitPolicyApplication{
id: PolicyId.new(org_id, policy_type, application_id),
submitted_by: submitted_by,
insured: insured,
buyer: buyer,
insured_object: insured_object,
selected_providers: providers
}
end
@@ -147,7 +173,7 @@ defmodule PolicyServiceWeb.PolicyController do
)
def accept(conn, %{"application_id" => application_id} = params) do
org_id = conn.assigns[:org_id] || "test"
org_id = conn.private[PolicyServiceWeb.Plugs.ExtractOrganizationId]
with {:ok, policy} <- PolicyQueries.get_by_application_id(org_id, application_id) do
command =
@@ -155,9 +181,29 @@ defmodule PolicyServiceWeb.PolicyController 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"] || %{}
accepted_by: params["accepted_by"] || "system",
accepted_plan_id: params["accepted_plan_id"]
}
"life" ->
%LifePolicy.AcceptQuoteAndSolicit{
id: PolicyId.new(org_id, policy.policy_type, application_id),
accepted_by: params["accepted_by"] || "system",
accepted_plan_id: params["accepted_plan_id"]
}
"fire_structure" ->
%FireStructurePolicy.AcceptQuoteAndSolicit{
id: PolicyId.new(org_id, policy.policy_type, application_id),
accepted_by: params["accepted_by"] || "system",
accepted_plan_id: params["accepted_plan_id"]
}
"fire_contents" ->
%FireContentsPolicy.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
@@ -166,9 +212,6 @@ defmodule PolicyServiceWeb.PolicyController do
{: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"})
@@ -181,54 +224,6 @@ defmodule PolicyServiceWeb.PolicyController do
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
# ---------------------------------------------------------------------------
@@ -238,9 +233,10 @@ defmodule PolicyServiceWeb.PolicyController 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,
insured: p.insured,
buyer: p.buyer,
insured_object: p.insured_object,
provider_policy_number: p.provider_policy_number,
submitted_at: p.submitted_at
}
end
@@ -252,17 +248,14 @@ defmodule PolicyServiceWeb.PolicyController do
submitted_by: p.submitted_by,
policy_type: p.policy_type,
status: p.status,
applicant_info: p.applicant_info,
policy_details: p.policy_details,
insured: p.insured,
buyer: p.buyer,
insured_object: p.insured_object,
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,
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,
@@ -287,12 +280,15 @@ defmodule PolicyServiceWeb.PolicyController do
# Parse helpers
# ---------------------------------------------------------------------------
defp parse_policy_type(type) when type in ["car", "life", "fire"], do: {:ok, type}
defp parse_policy_type(type) when type in ["car", "life", "fire_structure", "fire_contents"],
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
# insured — individual
defp parse_insured(info) do
case info["type"] do
"individual" ->
case info["date_of_birth"] do
nil ->
{:error, :missing_date_of_birth}
@@ -300,68 +296,145 @@ defmodule PolicyServiceWeb.PolicyController do
dob ->
{:ok,
%{
"type" => "individual",
"name" => info["name"],
"date_of_birth" => dob,
"document_id" => doc
"document_id" => info["document_id"],
"gender" => info["gender"],
"email" => info["email"],
"phone" => info["phone"],
"address" => info["address"]
}}
end
end
# corporate — has ruc
defp parse_applicant_info(%{"ruc" => ruc} = info)
when is_binary(ruc) and byte_size(ruc) > 0 do
"corporate" ->
{:ok,
%{
"type" => "corporate",
"company_name" => info["company_name"],
"ruc" => ruc,
"ruc" => info["ruc"],
"legal_rep_name" => info["legal_rep_name"],
"legal_rep_document" => info["legal_rep_document"]
"legal_rep_document" => info["legal_rep_document"],
"email" => info["email"],
"phone" => info["phone"],
"address" => info["address"]
}}
_ ->
{:error, :invalid_insured_type}
end
end
# buyer — individual
defp parse_buyer(info) do
case info["type"] do
"individual" ->
case info["date_of_birth"] do
nil ->
{:error, :missing_date_of_birth}
dob ->
{:ok,
%{
"type" => "individual",
"name" => info["name"],
"date_of_birth" => dob,
"document_id" => info["document_id"],
"email" => info["email"],
"phone" => info["phone"],
"address" => info["address"]
}}
end
defp parse_applicant_info(_), do: {:error, :invalid_applicant_info}
"corporate" ->
{:ok,
%{
"type" => "corporate",
"company_name" => info["company_name"],
"ruc" => info["ruc"],
"legal_rep_name" => info["legal_rep_name"],
"legal_rep_document" => info["legal_rep_document"],
"email" => info["email"],
"phone" => info["phone"],
"address" => info["address"]
}}
_ ->
{:error, :invalid_buyer_type}
end
end
# individual — has document_id
# car details
defp parse_policy_details("car", nil), do: {:error, :missing_policy_details}
defp parse_insured_object("car", nil), do: {:error, :missing_insured_object}
defp parse_policy_details("car", d) do
defp parse_insured_object("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"]
"engine_number" => d["engine_number"],
"rc_limits" => %{
"bodily_injury" => d["rc_limits"]["bodily_injury"],
"property_damage" => d["rc_limits"]["property_damage"]
},
"market_value" => d["market_value"],
"requested_value" => d["requested_value"]
}}
end
# life details
defp parse_policy_details("life", nil), do: {:error, :missing_policy_details}
defp parse_insured_object("life", nil), do: {:error, :missing_insured_object}
defp parse_policy_details("life", d) do
defp parse_insured_object("life", d) do
{:ok,
%{
"coverage_type" => d["coverage_type"],
"coverage_amount" => d["coverage_amount"],
"beneficiary" => d["beneficiary"]
"coverage_years" => d["coverage_years"],
"smoker" => d["smoker"],
"medications" => d["medications"] || [],
"surgeries" => d["surgeries"] || [],
"weight" => d["weight"],
"height" => d["height"]
}}
end
# fire details
defp parse_policy_details("fire", nil), do: {:error, :missing_policy_details}
# fire_structure details
defp parse_insured_object("fire_structure", nil), do: {:error, :missing_insured_object}
defp parse_policy_details("fire", d) do
defp parse_insured_object("fire_structure", d) do
{:ok,
%{
"property_address" => d["property_address"],
"property_value" => d["property_value"]
"location" => d["location"],
"property_value" => d["property_value"],
"property_use" => d["property_use"],
"security_measures" => d["security_measures"] || [],
"market_value" => d["market_value"]
}}
end
defp parse_policy_details(_, _), do: {:error, :invalid_policy_details}
# fire_contents details
defp parse_insured_object("fire_contents", nil), do: {:error, :missing_insured_object}
defp parse_insured_object("fire_contents", d) do
{:ok,
%{
"location" => d["location"],
"contents_value" => d["contents_value"],
"property_use" => d["property_use"],
"security_measures" => d["security_measures"] || [],
"high_value_items" => d["high_value_items"] || []
}}
end
defp parse_insured_object(_, _), do: {:error, :invalid_insured_object}
defp parse_providers(nil), do: {:error, :missing_providers}
defp parse_providers([]), do: {:error, :no_providers_selected}
@@ -371,8 +444,4 @@ defmodule PolicyServiceWeb.PolicyController do
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

View File

@@ -42,9 +42,13 @@ defmodule PolicyServiceWeb.Endpoint do
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug CORSPlug, origin: ["http://localhost:3000"]
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug CORSPlug,
origin: ["*"],
headers: ["*"]
plug PolicyServiceWeb.Router
end

View File

@@ -0,0 +1,81 @@
defmodule PolicyServiceWeb.Plugs.AuthorizeRoles do
@moduledoc """
Authorize request based on Zitadel role permissions.
After token introspection, checks if the user holds any of the
`required_permissions` roles for the organization identified by
`X-Organization-Id` header.
The Zitadel roles claim structure is:
%{"urn:zitadel:iam:org:project:<project_id>:roles": {
"<role>": {
"<org_id>": "<org_domain>"
},
"<role>": {
"<org_id>": "<org_domain>"
}
}}
"""
@behaviour Plug
import Plug.Conn
@impl Plug
def init(opts),
do:
opts
|> Keyword.validate!([
:roles_claim
])
@impl Plug
def call(conn, opts) do
if authorized?(
conn,
Keyword.get(opts, :roles_claim),
Keyword.get(opts, :required_permissions)
) do
conn
else
conn
|> put_resp_content_type("application/json")
|> halt()
|> send_resp(
:forbidden,
%{error: "Forbidden", reason: "Missing required role"}
)
end
end
defp authorized?(conn, roles_claim, required_permissions) do
org_id = conn.private[PolicyServiceWeb.Plugs.ExtractOrganizationId]
with true <- org_id_given?(org_id),
roles_map <- get_roles_map(conn, roles_claim),
true <- has_any_role?(roles_map, org_id, required_permissions) do
true
else
_ -> false
end
end
defp org_id_given?(org_id), do: not is_nil(org_id)
defp get_roles_map(conn, roles_claim) do
case conn.private[Oidcc.Plug.IntrospectToken] do
%Oidcc.TokenIntrospection{extra: extra} ->
Map.get(extra, roles_claim, %{})
_ ->
%{}
end
end
defp has_any_role?(roles_map, org_id, required_permissions) do
Enum.any?(required_permissions, fn role ->
role_orgs = Map.get(roles_map, role, %{})
Map.has_key?(role_orgs, org_id)
end)
end
end

View File

@@ -0,0 +1,22 @@
defmodule PolicyServiceWeb.Plugs.ExtractOrganizationId do
@moduledoc """
Extract `X-Organization-Id` request header.
Stores the organization identifier in conn.private[__MODULE__] for downstream authorization checks.
"""
@behaviour Plug
import Plug.Conn, only: [get_req_header: 2, put_private: 3]
@impl Plug
def init(_opts), do: %{}
@impl Plug
def call(conn, _opts) do
case get_req_header(conn, "x-organization-id") do
[org_id | _rest] -> put_private(conn, __MODULE__, org_id)
[] -> put_private(conn, __MODULE__, nil)
end
end
end

View File

@@ -0,0 +1,27 @@
defmodule PolicyServiceWeb.Plugs.RequireOrganizationId do
@moduledoc """
Ensure `X-Organization-Id` header is provided.
This plug must be used after `PolicyServiceWeb.Plugs.ExtractOrganizationId`.
"""
@behaviour Plug
import Plug.Conn, only: [get_req_header: 2, halt: 1, send_resp: 3]
@impl Plug
def init(_opts), do: %{}
@impl Plug
def call(conn, _opts) do
case get_req_header(conn, "x-organization-id") do
[] ->
conn
|> halt()
|> send_resp(:bad_request, "The organization id is required")
[_org_id] ->
conn
end
end
end

View File

@@ -0,0 +1,34 @@
defmodule PolicyServiceWeb.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

@@ -8,26 +8,84 @@ defmodule PolicyServiceWeb.Router do
plug OpenApiSpex.Plug.PutApiSpec, module: PolicyServiceWeb.ApiSpec
end
scope "/api" do
pipe_through [:api]
pipeline :auth do
plug Oidcc.Plug.ExtractAuthorization
plug Oidcc.Plug.RequireAuthorization
plug PolicyServiceWeb.Plugs.RequireOrganizationId
plug PolicyServiceWeb.Plugs.ExtractOrganizationId
plug :introspect
end
pipeline :read do
plug :authorize_roles, required_permissions: ["policy:read"]
end
pipeline :submit_solicitation do
plug :authorize_roles, required_permissions: ["policy:submit_solicitation"]
end
pipeline :create_request do
plug :authorize_roles, required_permissions: ["policy:create_request"]
end
get "/health", HealthController, :health
get "/health/ready", HealthController, :ready
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
scope "/v1" do
get "/policies", PolicyController, :index
get "/policies/:application_id", PolicyController, :show
post "/policies", PolicyController, :create
post "/policies/:application_id/accept", PolicyController, :accept
get "/policies/:application_id/solicitation-url", PolicyController, :solicitation_url
end
end
if Mix.env() == :dev do
scope "/swaggerui" do
get "/", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
end
scope "/api" do
pipe_through [:api]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
scope "/v1" do
pipe_through [:auth]
scope "/" do
pipe_through [:read]
get "/policies", PolicyController, :index
get "/policies/:application_id", PolicyController, :show
end
scope "/" do
pipe_through [:create_request]
post "/policies", PolicyController, :create
end
scope "/" do
pipe_through [:submit_solicitation]
post "/policies/:application_id/accept", PolicyController, :accept
end
end
end
def introspect(conn, _opts) do
zitadel = Application.get_env(:policy_service, :zitadel)
opts =
Oidcc.Plug.IntrospectToken.init(
provider: PolicyService.ZitadelProvider,
client_id: zitadel[:client_id],
client_secret: zitadel[:client_secret],
token_introspection_opts: %{client_self_only: false}
)
Oidcc.Plug.IntrospectToken.call(
conn,
opts
)
end
def authorize_roles(conn, opts) do
zitadel = Application.get_env(:policy_service, :zitadel)
o =
PolicyServiceWeb.Plugs.AuthorizeRoles.init(roles_claim: zitadel[:roles_claim])
PolicyServiceWeb.Plugs.AuthorizeRoles.call(conn, Keyword.merge(opts, o))
end
end

View File

@@ -18,8 +18,105 @@ defmodule PolicyServiceWeb.Schemas.Policy do
})
end
defmodule InsuredIndividual do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "InsuredIndividual",
type: :object,
required: [:type, :name, :date_of_birth, :document_id, :gender],
properties: %{
type: %Schema{type: :string, enum: ["individual"]},
name: %Schema{type: :string, example: "Juan Pérez"},
date_of_birth: %Schema{type: :string, format: :date, example: "1985-06-15"},
document_id: %Schema{type: :string, example: "8-123-456"},
gender: %Schema{type: :string, enum: ["male", "female"], example: "male"},
email: %Schema{type: :string, format: :email, example: "juan@example.com"},
phone: %Schema{type: :string, example: "+507-1234-5678"},
address: %Schema{type: :string, example: "Calle 50, Panama City"}
}
})
end
defmodule InsuredCorporate do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "InsuredCorporate",
type: :object,
required: [:type, :company_name, :ruc, :legal_rep_name, :legal_rep_document],
properties: %{
type: %Schema{type: :string, enum: ["corporate"]},
company_name: %Schema{type: :string, example: "Empresa ABC S.A."},
ruc: %Schema{type: :string, example: "123456-1-123456"},
legal_rep_name: %Schema{type: :string, example: "María García"},
legal_rep_document: %Schema{type: :string, example: "8-456-789"},
email: %Schema{type: :string, format: :email, example: "contact@empresa-abc.com"},
phone: %Schema{type: :string, example: "+507-1234-5678"},
address: %Schema{type: :string, example: "Calle 50, Panama City"}
}
})
end
defmodule Insured do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Insured",
oneOf: [InsuredIndividual, InsuredCorporate]
})
end
defmodule BuyerIndividual do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "BuyerIndividual",
type: :object,
required: [:type, :name, :date_of_birth, :document_id],
properties: %{
type: %Schema{type: :string, enum: ["individual"]},
name: %Schema{type: :string, example: "María García"},
date_of_birth: %Schema{type: :string, format: :date, example: "1980-03-20"},
document_id: %Schema{type: :string, example: "8-456-789"},
email: %Schema{type: :string, format: :email, example: "maria@example.com"},
phone: %Schema{type: :string, example: "+507-8765-4321"},
address: %Schema{type: :string, example: "Calle 75, Panama City"}
}
})
end
defmodule BuyerCorporate do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "BuyerCorporate",
type: :object,
required: [:type, :company_name, :ruc, :legal_rep_name, :legal_rep_document],
properties: %{
type: %Schema{type: :string, enum: ["corporate"]},
company_name: %Schema{type: :string, example: "Empresa XYZ S.A."},
ruc: %Schema{type: :string, example: "987654-1-987654"},
legal_rep_name: %Schema{type: :string, example: "Carlos López"},
legal_rep_document: %Schema{type: :string, example: "8-789-012"},
email: %Schema{type: :string, format: :email, example: "carlos@empresa-xyz.com"},
phone: %Schema{type: :string, example: "+507-8765-4321"},
address: %Schema{type: :string, example: "Calle 100, Panama City"}
}
})
end
defmodule Buyer do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Buyer",
oneOf: [BuyerIndividual, BuyerCorporate]
})
end
# ---------------------------------------------------------------------------
# Applicant — discriminated by presence of keys
# Policy details — one per policy type
# ---------------------------------------------------------------------------
defmodule ApplicantIndividual do
@@ -66,29 +163,28 @@ defmodule PolicyServiceWeb.Schemas.Policy do
# Policy details — one per policy type
# ---------------------------------------------------------------------------
defmodule CarPolicyDetails do
defmodule CarInsuredObject do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "CarPolicyDetails",
title: "CarInsuredObject",
type: :object,
required: [
:plate,
:make,
:model,
:year,
:car_value,
:use_type,
:car_type,
:chassis_number,
:engine_number
:rc_limits,
:market_value,
:requested_value
],
properties: %{
plate: %Schema{type: :string, example: "ABC-1234"},
make: %Schema{type: :string, example: "Toyota"},
model: %Schema{type: :string, example: "Corolla"},
year: %Schema{type: :integer, example: 2022},
car_value: %Schema{type: :number, example: 18000},
use_type: %Schema{type: :string, enum: ["private", "commercial", "bus", "taxi", "school"]},
car_type: %Schema{
type: :string,
@@ -105,45 +201,106 @@ defmodule PolicyServiceWeb.Schemas.Policy do
]
},
chassis_number: %Schema{type: :string, example: "9BWZZZ377VT004251"},
engine_number: %Schema{type: :string, example: "1NZ-FE-1234567"}
engine_number: %Schema{type: :string, example: "1NZ-FE-1234567"},
rc_limits: %Schema{
type: :object,
properties: %{
bodily_injury: %Schema{type: :number, example: 50000},
property_damage: %Schema{type: :number, example: 25000}
}
},
market_value: %Schema{type: :number, example: 18000},
requested_value: %Schema{type: :number, example: 20000}
}
})
end
defmodule LifePolicyDetails do
defmodule LifeInsuredObject do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "LifePolicyDetails",
title: "LifeInsuredObject",
type: :object,
required: [:coverage_amount, :beneficiary],
required: [:coverage_type, :coverage_amount, :coverage_years, :smoker],
properties: %{
coverage_type: %Schema{
type: :string,
enum: ["banking", "protection"]
},
coverage_amount: %Schema{type: :number, example: 100_000},
beneficiary: %Schema{type: :string, example: "María Pérez"}
coverage_years: %Schema{type: :integer, example: 10},
smoker: %Schema{type: :boolean, example: false},
medications: %Schema{
type: :array,
items: %Schema{type: :string},
example: ["Aspirin", "Lisinopril"]
},
surgeries: %Schema{
type: :array,
items: %Schema{type: :string},
example: ["Appendectomy, 2015"]
},
weight: %Schema{type: :number, example: 70},
height: %Schema{type: :number, example: 175}
}
})
end
defmodule FirePolicyDetails do
defmodule FireStructureInsuredObject do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "FirePolicyDetails",
title: "FireStructureInsuredObject",
type: :object,
required: [:property_address, :property_value],
required: [:location, :property_value, :property_use, :security_measures, :market_value],
properties: %{
property_address: %Schema{type: :string, example: "Calle 50, Panama City"},
property_value: %Schema{type: :number, example: 250_000}
location: %Schema{type: :string, example: "Calle 50, Panama City"},
property_value: %Schema{type: :number, example: 250_000},
property_use: %Schema{type: :string, example: "residential"},
security_measures: %Schema{type: :array, items: %Schema{type: :string}},
market_value: %Schema{type: :number, example: 260_000}
}
})
end
defmodule PolicyDetails do
defmodule FireContentsInsuredObject do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "PolicyDetails",
oneOf: [CarPolicyDetails, LifePolicyDetails, FirePolicyDetails]
title: "FireContentsInsuredObject",
type: :object,
required: [:location, :contents_value, :property_use, :security_measures],
properties: %{
location: %Schema{type: :string, example: "Calle 50, Panama City"},
contents_value: %Schema{type: :number, example: 50_000},
property_use: %Schema{type: :string, example: "residential"},
security_measures: %Schema{type: :array, items: %Schema{type: :string}},
high_value_items: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
description: %Schema{type: :string},
value: %Schema{type: :number},
type: %Schema{type: :string, enum: ["electronic", "other"]}
}
}
}
}
})
end
defmodule InsuredObject do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "InsuredObject",
oneOf: [
CarInsuredObject,
LifeInsuredObject,
FireStructureInsuredObject,
FireContentsInsuredObject
]
})
end
@@ -207,15 +364,16 @@ defmodule PolicyServiceWeb.Schemas.Policy do
OpenApiSpex.schema(%{
title: "CreatePolicyRequest",
type: :object,
required: [:policy_type, :applicant_info, :policy_details, :selected_providers],
required: [:policy_type, :insured, :buyer, :insured_object, :selected_providers],
properties: %{
policy_type: %Schema{
type: :string,
enum: ["car", "life", "fire"],
description: "Determines the shape of policy_details"
enum: ["car", "life", "fire_structure", "fire_contents"],
description: "Determines the shape of insured_object"
},
applicant_info: ApplicantInfo,
policy_details: PolicyDetails,
insured: Insured,
buyer: Buyer,
insured_object: InsuredObject,
selected_providers: %Schema{type: :array, items: SelectedProvider, minItems: 1}
}
})
@@ -227,15 +385,11 @@ defmodule PolicyServiceWeb.Schemas.Policy do
OpenApiSpex.schema(%{
title: "AcceptQuoteRequest",
type: :object,
required: [:quote_id, :plan_id],
required: [:accepted_plan_id],
properties: %{
quote_id: %Schema{type: :string},
plan_id: %Schema{type: :string},
solicitation_fields: %Schema{
type: :object,
additionalProperties: %Schema{type: :string},
description: "Optional flat map of AcroForm field names to values",
nullable: true
accepted_plan_id: %Schema{
type: :string,
description: "Plan ID to accept"
}
}
})
@@ -258,20 +412,6 @@ defmodule PolicyServiceWeb.Schemas.Policy do
})
end
defmodule SolicitationUrlResponse do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "SolicitationUrlResponse",
type: :object,
properties: %{
download_url: %Schema{type: :string},
s3_key: %Schema{type: :string},
version: %Schema{type: :integer}
}
})
end
defmodule PolicySummary do
require OpenApiSpex
@@ -280,14 +420,18 @@ defmodule PolicyServiceWeb.Schemas.Policy do
type: :object,
properties: %{
application_id: %Schema{type: :string},
policy_type: %Schema{type: :string, enum: ["car", "life", "fire"]},
policy_type: %Schema{
type: :string,
enum: ["car", "life", "fire_structure", "fire_contents"]
},
status: %Schema{
type: :string,
enum: ["quote_requested", "quotes_received", "solicitation_sent", "issued"]
enum: ["quote_requested", "quotes_received", "awaiting_policy", "issued"]
},
applicant_info: ApplicantInfo,
policy_details: PolicyDetails,
policy_number: %Schema{type: :string, nullable: true},
insured: Insured,
buyer: Buyer,
insured_object: InsuredObject,
provider_policy_number: %Schema{type: :string, nullable: true},
submitted_at: %Schema{type: :string, format: :"date-time"}
}
})
@@ -303,22 +447,22 @@ defmodule PolicyServiceWeb.Schemas.Policy do
application_id: %Schema{type: :string},
org_id: %Schema{type: :string},
submitted_by: %Schema{type: :string},
policy_type: %Schema{type: :string, enum: ["car", "life", "fire"]},
policy_type: %Schema{
type: :string,
enum: ["car", "life", "fire_structure", "fire_contents"]
},
status: %Schema{
type: :string,
enum: ["quote_requested", "quotes_received", "solicitation_sent", "issued"]
enum: ["quote_requested", "quotes_received", "awaiting_policy", "issued"]
},
applicant_info: ApplicantInfo,
policy_details: PolicyDetails,
insured: Insured,
buyer: Buyer,
insured_object: InsuredObject,
selected_providers: %Schema{type: :array, items: %Schema{type: :string}},
quotes: %Schema{type: :object, additionalProperties: QuoteData},
accepted_quote_id: %Schema{type: :string, nullable: true},
accepted_plan_id: %Schema{type: :string, nullable: true},
accepted_provider_id: %Schema{type: :string, nullable: true},
accepted_at: %Schema{type: :string, format: :"date-time", nullable: true},
solicitation_id: %Schema{type: :string, nullable: true},
solicitation_s3_key: %Schema{type: :string, nullable: true},
policy_number: %Schema{type: :string, nullable: true},
accepted_by: %Schema{type: :string, nullable: true},
provider_policy_number: %Schema{type: :string, nullable: true},
premium: %Schema{type: :number, nullable: true},
effective_date: %Schema{type: :string, format: :date, nullable: true},
expiry_date: %Schema{type: :string, format: :date, nullable: true},

20
mix.exs
View File

@@ -10,7 +10,11 @@ defmodule PolicyService.MixProject do
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
listeners: [Phoenix.CodeReloader]
listeners: [Phoenix.CodeReloader],
dialyzer: [
plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
plt_add_apps: [:mix, :ex_unit]
]
]
end
@@ -56,7 +60,11 @@ defmodule PolicyService.MixProject do
{:open_api_spex, "~> 3.20"},
{:cors_plug, "~> 3.0"},
{:flop, "~> 0.26"},
{:req, "~> 0.5"}
{:req, "~> 0.5"},
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
{:typedstruct, "~> 0.5"},
{:oidcc, "~> 3.7"},
{:oidcc_plug, "~> 0.4"}
]
end
@@ -72,7 +80,13 @@ defmodule PolicyService.MixProject do
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"]
precommit: [
"compile --warnings-as-errors",
"deps.unlock --unused",
"dialyzer",
"format",
"test"
]
]
end
end

View File

@@ -10,9 +10,11 @@
"credentials_obfuscation": {:hex, :credentials_obfuscation, "3.5.0", "61e282adfb4439486b3994faaec69543c7ee6cc7e70c6340e8853fd9deaf8219", [:rebar3], [], "hexpm", "843adbe3246861ce0f1a0fa3222f384834eb31defd8d6b9cba7afd2977c957bc"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
"ecto_sql": {:hex, :ecto_sql, "3.13.4", "b6e9d07557ddba62508a9ce4a484989a5bb5e9a048ae0e695f6d93f095c25d60", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b38cf0749ca4d1c5a8bcbff79bbe15446861ca12a61f9fba604486cb6b62a14"},
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
"eventstore": {:hex, :eventstore, "1.4.8", "26778c991cfb078f3906a4267060efc7bb5e5943f69ddb8ae6fb60f07042a66e", [:mix], [{:fsm, "~> 0.3", [hex: :fsm, repo: "hexpm", optional: false]}, {:gen_stage, "~> 1.2", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "30c914602fdea8db5992a90ecb1f84068531e764cf0c066be71ff0eec4e3bcb9"},
"exconstructor": {:hex, :exconstructor, "1.3.1", "2c8b19b4702b195782e0cba46c7764df815c0beb8633383a9afb01199c47c3bd", [:mix], [], "hexpm", "5b7b2b043023e4643a44a66750d47587f01f3459d2fb4e7de05406b3a093fa6e"},
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
@@ -21,10 +23,13 @@
"gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"oidcc": {:hex, :oidcc, "3.7.2", "2047949832ca7984d6d9c218cc5f23e8096bf50ebb809124d3a01673ee2bfe12", [:mix, :rebar3], [{:igniter, "~> 0.6.3 or ~> 0.7.0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.3.1", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "e3f1ed91509fdeb31ec8b9de4ecda0e80cb68b463a9f5b7a9ee1ee40e521e445"},
"oidcc_plug": {:hex, :oidcc_plug, "0.4.0", "e31ed82f44c0a1685874f7a8574d3ce714603d398c449b8b0c55e89908623979", [:mix], [{:igniter, "~> 0.5.50 or ~> 0.6.0 or ~> 0.7.0", [hex: :igniter, repo: "hexpm", optional: true]}, {:oidcc, "~> 3.7", [hex: :oidcc, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "4d3d6da5f4b51bd9ffc03e4539c631503d459153e6ba31964316c87f4a310068"},
"open_api_spex": {:hex, :open_api_spex, "3.22.2", "0b3c4f572ee69cb6c936abf426b9d84d8eebd34960871fd77aead746f0d69cb0", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "0a4fc08472d75e9cfe96e0748c6b1565b3b4398f97bf43fcce41b41b6fd3fb33"},
"phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
@@ -43,6 +48,7 @@
"telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"},
"thoas": {:hex, :thoas, "1.2.1", "19a25f31177a17e74004d4840f66d791d4298c5738790fa2cc73731eb911f195", [:rebar3], [], "hexpm", "e38697edffd6e91bd12cea41b155115282630075c2a727e7a6b2947f5408b86a"},
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"typedstruct": {:hex, :typedstruct, "0.5.4", "d1d33d58460a74f413e9c26d55e66fd633abd8ac0fb12639add9a11a60a0462a", [:make, :mix], [], "hexpm", "ffaef36d5dbaebdbf4ed07f7fb2ebd1037b2c1f757db6fb8e7bcbbfabbe608d8"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
}

View File

@@ -9,15 +9,26 @@ controllers:
repository: gitea.corredorconect.com/software-engineering/policy-service
tag: '{{ $.Chart.AppVersion }}'
command:
- /bin/sh
- -c
- "mix ecto.create && mix ecto.migrate && mix event_store.create && mix event_store.init"
- "/bin/policy_service"
args:
- "eval"
- "PolicyService.Release.migrate"
env:
MIX_ENV: prod
SECRET_KEY_BASE:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-secrets'
key: secretKeyBase
RELEASE_COOKIE:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-secrets'
key: cookie
DATABASE_URL:
valueFrom:
secretKeyRef:
name: policy-service-pg-app
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-cluster-pg-app'
key: uri
containers:
main:
@@ -25,14 +36,61 @@ controllers:
repository: gitea.corredorconect.com/software-engineering/policy-service
tag: '{{ $.Chart.AppVersion }}'
env:
LOG_LEVEL: debug
MIX_ENV: prod
PORT: "8080"
CORS_ORIGIN:
value: "*"
PHX_HOST: "0.0.0.0"
PHX_SERVER: "true"
RABBITMQ_HOST:
value: "rabbitmq.rabbitmq.svc.cluster.local"
RABBITMQ_VHOST:
value: "application"
RABBITMQ_USERNAME:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-rabbitmq-user-user-credentials'
key: username
RABBITMQ_PASSWORD:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-rabbitmq-user-user-credentials'
key: password
RELEASE_COOKIE:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-secrets'
key: cookie
SECRET_KEY_BASE:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-secrets'
key: secretKeyBase
DATABASE_URL:
valueFrom:
secretKeyRef:
name: policy-service-pg-app
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-cluster-pg-app'
key: uri
# Zitadel Configuration
ZITADEL_ISSUER:
value: "https://id.corredorconect.com"
ZITADEL_CLIENT_ID:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-apiapp-client-secret'
key: clientId
ZITADEL_CLIENT_SECRET:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-apiapp-client-secret'
key: clientSecret
ZITADEL_PROJECT_ID:
valueFrom:
secretKeyRef:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-apiapp-client-secret'
key: projectId
probes:
liveness:
enabled: true
@@ -52,31 +110,135 @@ controllers:
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
# resources:
# requests:
# cpu: 100m
# memory: 256Mi
# limits:
# cpu: 500m
# memory: 512Mi
service:
main:
enabled: true
controller: main
primary: true
type: ClusterIP
ports:
http:
enabled: true
primary: true
port: 8080
protocol: HTTP
# PostgreSQL Cluster - managed externally via CNPG operator
# The secret policy-service-pg-app will be created by CNPG
rawResources:
rabbitmq-user:
enabled: true
apiVersion: rabbitmq.com/v1beta1
kind: User
sufix: rabbitmq-user
spec:
spec:
rabbitmqClusterReference:
name: rabbitmq
namespace: rabbitmq
tags:
- administrator
rabbitmq-user-permission:
enabled: true
apiVersion: rabbitmq.com/v1beta1
kind: Permission
sufix: rabbitmq-user-permission
spec:
spec:
rabbitmqClusterReference:
name: rabbitmq
namespace: rabbitmq
vhost: "application"
userReference:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-rabbitmq-user'
permissions:
write: ".*"
configure: ".*"
read: ".*"
exchange-quote-requested:
enabled: true
apiVersion: rabbitmq.com/v1beta1
kind: Exchange
suffix: exchange-quote-requested
spec:
spec:
name: policy_service.events.quote_requested
type: topic
durable: true
vhost: "application"
rabbitmqClusterReference:
name: rabbitmq
namespace: rabbitmq
exchange-solicitation-requested:
enabled: true
apiVersion: rabbitmq.com/v1beta1
kind: Exchange
suffix: exchange-solicitation-requested
spec:
spec:
name: policy_service.events.solicitation_requested
type: topic
durable: true
vhost: "application"
rabbitmqClusterReference:
name: rabbitmq
namespace: rabbitmq
exchange-solicitation-task-completed:
enabled: true
apiVersion: rabbitmq.com/v1beta1
kind: Exchange
suffix: exchange-solicitation-task-completed
spec:
spec:
name: workload_service.events.solicitation_task_completed
type: topic
durable: true
vhost: "application"
rabbitmqClusterReference:
name: rabbitmq
namespace: rabbitmq
password-generator:
enabled: true
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
suffix: password-generator
spec:
spec:
length: 32
noUpper: false
allowRepeat: true
secretKeys:
- cookie
- secretKeyBase
external-secret:
enabled: true
apiVersion: external-secrets.io/v1
kind: ExternalSecret
suffix: secrets
spec:
spec:
refreshInterval: 0s
secretStoreRef:
name: cluster-secrets-store
kind: ClusterSecretStore
target:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-secrets'
creationPolicy: Owner
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-password-generator'
cluster:
enabled: true
apiVersion: postgresql.cnpg.io/v1
@@ -92,10 +254,31 @@ rawResources:
owner: policy_service
storage:
size: 5Gi
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
database:
enabled: true
apiVersion: postgresql.cnpg.io/v1
kind: Database
suffix: database
spec:
spec:
name: policy_service
owner: policy_service
cluster:
name: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-cluster-pg'
schemas:
- name: eventstore
owner: policy_service
apiapp:
enabled: true
apiVersion: zitadel.github.com/v1alpha1
kind: APIApp
suffix: apiapp
spec:
spec:
projectRef:
name: seguros-dev
namespace: zitadel-resources-operator
apiAppName: policy-service
authMethodType: API_AUTH_METHOD_TYPE_BASIC

View File

@@ -7,32 +7,28 @@ defmodule PolicyService.Repo.Migrations.CreatePolicyApplications do
add :application_id, :string, null: false
add :org_id, :string, null: false
add :submitted_by, :string, null: false
# "car" | "life" | "fire"
# "car" | "life" | "fire_structure" | "fire_contents"
add :policy_type, :string, null: false
# Applicant — full map, shape varies by individual vs corporate
add :applicant_info, :map, default: %{}
# Insured — full map, shape varies by individual vs corporate
add :insured, :map, default: %{}
# Policy-type-specific details — shape varies by policy_type
add :policy_details, :map, default: %{}
# Buyer — full map, shape varies by individual vs corporate
add :buyer, :map, default: %{}
# Insured object — policy-type-specific details, shape varies by policy_type
add :insured_object, :map, default: %{}
# Providers + quotes
add :selected_providers, {:array, :string}, default: []
add :quotes, :map, default: %{}
# Accepted plan
add :accepted_quote_id, :string
add :accepted_plan_id, :string
add :accepted_provider_id, :string
add :accepted_by, :string
add :accepted_at, :utc_datetime_usec
# Solicitation
add :solicitation_id, :string
add :solicitation_s3_key, :string
# Issued policy
add :policy_number, :string
add :provider_policy_number, :string
add :premium, :decimal
add :effective_date, :date
add :expiry_date, :date

38
rel/vm.args.eex Normal file
View File

@@ -0,0 +1,38 @@
## --- memory optimisation (embedded/low-RAM targets) ---
## disable carrier utilization limit
+MBacul 0
+MHacul 0
## smaller carrier sizes
+MBsmbcs 64
+MBlmbcs 128
+MHsmbcs 64
+MHlmbcs 128
## smaller main carrier
+MMscs 20
## --- scheduler tuning ---
+S 1:1
+SDcpu 1:1
+SDio 1
## --- resource limits ---
+t 100000
+P 50000
+Q 8192
## --- general ---
+c false
+sbwt none
+sbwtdcpu none
+sbwtdio none
+swt very_low
+swtdcpu very_low
+swtdio very_low
+secio false
+K true