Compare commits

..

123 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
5e4f1f33c8 use chart app version for tag
All checks were successful
Build and Publish / build-release (push) Successful in 4m57s
2026-04-14 14:49:01 -05:00
cb29c5c762 image tag should be app version 2026-04-14 14:36:34 -05:00
29bc255f0f use busybox instead of bash
All checks were successful
Build and Publish / build-release (push) Successful in 5m3s
2026-04-14 14:26:31 -05:00
f7f594a2e4 add suffix to cluster name
All checks were successful
Build and Publish / build-release (push) Successful in 1m24s
2026-04-14 14:22:28 -05:00
d0feb9498b add sh to image
All checks were successful
Build and Publish / build-release (push) Successful in 1m28s
2026-04-14 14:09:11 -05:00
6a737eb182 remove unused values
All checks were successful
Build and Publish / build-release (push) Successful in 2m37s
2026-04-14 14:00:42 -05:00
4280d90467 remove image
All checks were successful
Build and Publish / build-release (push) Successful in 1m23s
2026-04-14 13:39:29 -05:00
2961dc2dbc fix values error
All checks were successful
Build and Publish / build-release (push) Successful in 1m21s
2026-04-14 13:12:43 -05:00
0ac142f69f try fix instances
All checks were successful
Build and Publish / build-release (push) Successful in 1m22s
2026-04-14 13:02:37 -05:00
f5747414f3 use raw resources for postgresql
All checks were successful
Build and Publish / build-release (push) Successful in 1m21s
2026-04-14 12:57:16 -05:00
a4af47f005 use only url from config
All checks were successful
Build and Publish / build-release (push) Successful in 1m27s
2026-04-14 12:39:39 -05:00
4dc2bfbf8e use correct config 2026-04-14 12:37:54 -05:00
ef5540e032 fix helm repo add
Some checks failed
Build and Publish / build-release (push) Failing after 3s
2026-04-14 12:31:26 -05:00
c0d1a7ab87 helm repo add
Some checks failed
Build and Publish / build-release (push) Failing after 1m30s
2026-04-14 12:28:57 -05:00
7fa1d98663 correctr dep build
Some checks failed
Build and Publish / build-release (push) Failing after 1m18s
2026-04-14 12:26:42 -05:00
560466ecc9 build deps before package
Some checks failed
Build and Publish / build-release (push) Failing after 1m17s
2026-04-14 12:24:13 -05:00
3a425d062b remove ls -la
Some checks failed
Build and Publish / build-release (push) Failing after 4m50s
2026-04-14 11:59:53 -05:00
6d636f6c68 ls before build
Some checks failed
Build and Publish / build-release (push) Failing after 3s
2026-04-14 11:33:02 -05:00
62bfb4c809 fix log flag
Some checks failed
Build and Publish / build-release (push) Failing after 3s
2026-04-14 11:24:39 -05:00
deb3fb6626 remove flakecheck
Some checks failed
Build and Publish / build-release (push) Failing after 3s
2026-04-14 11:20:53 -05:00
07b5b3d7a5 remove push to attic 2026-04-14 11:13:43 -05:00
25f08d7182 run on nix runner
Some checks failed
Build and Publish / build-release (push) Failing after 3s
2026-04-14 11:09:24 -05:00
dfe82d6c9c fix flag
Some checks failed
Build and Publish / build-release (push) Has been cancelled
2026-04-13 16:41:47 -05:00
fd47a80057 limit job parallelism
Some checks failed
Build and Publish / build-release (push) Failing after 4m35s
2026-04-13 16:35:45 -05:00
e145d3bb3e add health endpoints
Some checks are pending
Build and Publish / build-release (push) Waiting to run
2026-04-13 16:22:47 -05:00
3559699e40 fix check
Some checks are pending
Build and Publish / build-release (push) Waiting to run
2026-04-13 16:11:20 -05:00
7a7ce1bcbd use just busybox for image
Some checks failed
Build and Publish / build-release (push) Failing after 17m40s
2026-04-13 15:42:40 -05:00
044486ba74 use default storage class 2026-04-13 15:32:23 -05:00
51 changed files with 1871 additions and 803 deletions

View File

@@ -8,7 +8,7 @@ env:
IMAGE_NAME: ${{ github.event.repository.name }} IMAGE_NAME: ${{ github.event.repository.name }}
jobs: jobs:
build-release: build-release:
runs-on: ubuntu-latest runs-on: nix
permissions: permissions:
id-token: write id-token: write
contents: read contents: read
@@ -16,19 +16,6 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: DeterminateSystems/determinate-nix-action@v3
- uses: DeterminateSystems/flake-checker-action@main
with:
flake-lock-path: ./build/flake.lock
- name: Setup Attic cache
uses: ryanccn/attic-action@v0
with:
endpoint: ${{ secrets.ATTIC_ENDPOINT }}
cache: ${{ secrets.ATTIC_CACHE }}
token: ${{ secrets.ATTIC_TOKEN }}
- name: Build Docker Image via Nix Flake - name: Build Docker Image via Nix Flake
run: | run: |
nix build .#dockerImage --print-build-logs nix build .#dockerImage --print-build-logs
@@ -66,6 +53,8 @@ jobs:
- name: Package Helm Chart - name: Package Helm Chart
run: | run: |
VERSION=${{ github.run_number }} VERSION=${{ github.run_number }}
helm repo add bjw-s https://bjw-s-labs.github.io/helm-charts
helm dependency build ops/chart
helm package ops/chart --version $VERSION --app-version $VERSION helm package ops/chart --version $VERSION --app-version $VERSION
- name: Push Helm Chart to Gitea Registry - name: Push Helm Chart to Gitea Registry

67
.gitignore vendored
View File

@@ -1,3 +1,6 @@
/.direnv/
/result
# The directory Mix will write compiled artifacts to. # The directory Mix will write compiled artifacts to.
/_build/ /_build/
@@ -23,5 +26,67 @@ erl_crash.dump
/tmp/ /tmp/
# Ignore package tarball (built via "mix hex.build"). # 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 :open_api_spex, :cache_adapter, OpenApiSpex.Plug.NoneCache
config :policy_service, config :policy_service, :zitadel,
provider_service_url: "http://localhost:4002", issuer: System.get_env("ZITADEL_ISSUER", "https://id.corredorconect.com"),
solicitation_service_url: "http://localhost:8081" 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,17 +1,6 @@
import Config import Config
# Force using SSL in production. This also sets the "strict-security-transport" header, config :logger, level: :debug
# known as HSTS. If you have a health check endpoint, you may want to exclude it below.
# Note `:force_ssl` is required to be set at compile-time.
config :policy_service, PolicyServiceWeb.Endpoint,
force_ssl: [rewrite_on: [:x_forwarded_proto]],
exclude: [
# paths: ["/health"],
hosts: ["localhost", "127.0.0.1"]
]
# Do not print debug messages in production
config :logger, level: :info
# Runtime production configuration, including reading # Runtime production configuration, including reading
# of environment variables, is done on config/runtime.exs. # 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. # any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration. # 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 # ## Using releases
# #
# If you use `mix release`, you need to explicitly enable the server # 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 config :policy_service, PolicyServiceWeb.Endpoint, server: true
end end
config :policy_service, PolicyServiceWeb.Endpoint, if cookie = System.get_env("RELEASE_COOKIE") do
http: [port: String.to_integer(System.get_env("PORT", "4000"))] config :elixir, :cookie, cookie
end
if config_env() == :prod do if config_env() == :prod do
database_url = database_url =
@@ -34,18 +65,16 @@ if config_env() == :prod do
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: [] maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
config :policy_service, PolicyService.Repo, config :policy_service, PolicyService.Repo,
# ssl: true,
url: database_url, 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"),
# For machines with several cores, consider starting multiple pools of `pool_size`
# pool_count: 4,
socket_options: maybe_ipv6 socket_options: maybe_ipv6
# The secret key base is used to sign/encrypt cookies and other secrets. config :policy_service, PolicyService.EventStore,
# A default value is used in config/dev.exs and config/test.exs but you serializer: Commanded.Serialization.JsonSerializer,
# want to use a different value for prod and you most likely don't want url: database_url,
# to check this value into version control, so we use an environment schema: "eventstore",
# variable instead. pool_size: String.to_integer(System.get_env("EVENTSTORE_POOL_SIZE") || "1")
secret_key_base = secret_key_base =
System.get_env("SECRET_KEY_BASE") || System.get_env("SECRET_KEY_BASE") ||
raise """ raise """
@@ -58,45 +87,22 @@ if config_env() == :prod do
config :policy_service, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") config :policy_service, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
config :policy_service, PolicyServiceWeb.Endpoint, config :policy_service, PolicyServiceWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"], url: [host: host, port: String.to_integer(System.get_env("PORT", "4000")), scheme: "http"],
http: [ http: [
# Enable IPv6 and bind on all interfaces. ip: {0, 0, 0, 0, 0, 0, 0, 0},
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access. port: String.to_integer(System.get_env("PORT", "4000"))
# See the documentation on https://hexdocs.pm/bandit/Bandit.html#t:options/0
# for details about using IPv6 vs IPv4 and loopback vs public addresses.
ip: {0, 0, 0, 0, 0, 0, 0, 0}
], ],
secret_key_base: secret_key_base secret_key_base: secret_key_base
# ## SSL Support config :policy_service, :zitadel,
# issuer: System.get_env("ZITADEL_ISSUER", "https://id.corredorconect.com"),
# To get SSL working, you will need to add the `https` key client_id: System.get_env("ZITADEL_CLIENT_ID"),
# to your endpoint configuration: client_secret: System.get_env("ZITADEL_CLIENT_SECRET"),
# roles_claim: "urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:roles",
# config :policy_service, PolicyServiceWeb.Endpoint, required_scopes: [
# https: [ "openid",
# ..., "profile",
# port: 443, "urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:roles",
# cipher_suite: :strong, "urn:zitadel:iam:org:project:#{System.get_env("ZITADEL_PROJECT_ID")}:aud"
# keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"), ]
# certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
# ]
#
# The `cipher_suite` is set to `:strong` to support only the
# latest and more secure SSL ciphers. This means old browsers
# and clients may not be supported. You can set it to
# `:compatible` for wider support.
#
# `:keyfile` and `:certfile` expect an absolute path to the key
# and cert in disk or a relative path inside priv, for example
# "priv/ssl/server.key". For all supported SSL configuration
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
#
# We also recommend setting `force_ssl` in your config/prod.exs,
# ensuring no data is ever sent via http, always redirecting to https:
#
# config :policy_service, PolicyServiceWeb.Endpoint,
# force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.
end end

View File

@@ -21,7 +21,7 @@
mixFodDeps = beamPackages.fetchMixDeps { mixFodDeps = beamPackages.fetchMixDeps {
inherit pname version; inherit pname version;
src = ./.; src = ./.;
sha256 = "sha256-yqxq5pB7dKEhCZiJWXrKKCra45hxfyyfpP/zyNLEF7A="; sha256 = "sha256-YqPo8102nqTd6cAxt6O2R+nLLa9UfRLza4qojxoMtqM=";
}; };
package = beamPackages.mixRelease { package = beamPackages.mixRelease {
inherit pname version mixFodDeps; inherit pname version mixFodDeps;
@@ -29,19 +29,13 @@
meta = { meta = {
mainProgram = "policy_service"; mainProgram = "policy_service";
}; };
removeCookie = false;
}; };
dockerImage = pkgs.dockerTools.buildImage { dockerImage = pkgs.dockerTools.buildLayeredImage {
name = "policy_service"; name = "policy_service";
fromImageName = "hexpm/elixir"; contents = [ package pkgs.bashInteractive pkgs.busybox pkgs.dockerTools.caCertificates ];
fromImageTag = "1.17.5-erlang-27.0-debian-bookworm-20240612";
copyToRoot = pkgs.buildEnv {
name = "policy-service";
paths = [ package ];
pathsToLink = [ "/bin" ];
};
config = { config = {
Cmd = [ "/bin/bash" ]; Cmd = [ "${package}/bin/policy_service" "start" ];
WorkingDir = "/";
}; };
}; };
in in
@@ -52,7 +46,9 @@
buildInputs = with pkgs; [ buildInputs = with pkgs; [
elixir elixir
elixir-ls elixir-ls
kubernetes-helm
git git
nodejs
]; ];
}; };
} }

View File

@@ -6,29 +6,60 @@ defmodule PolicyService.Aggregates.CarPolicyApplication do
@valid_use_types ~w(private commercial bus taxi school) @valid_use_types ~w(private commercial bus taxi school)
@valid_car_types ~w(sedan suv hatchback coupe convertible pickup van minivan truck) @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, "plate" => plate,
"make" => make, "make" => make,
"model" => model, "model" => model,
"year" => year, "year" => year,
"car_value" => car_value,
"use_type" => use_type, "use_type" => use_type,
"car_type" => car_type, "car_type" => car_type,
"chassis_number" => chassis, "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 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 is_binary(chassis) and is_binary(engine) do
cond do cond do
year < 1886 or year > Date.utc_today().year + 1 -> {:error, :invalid_car_year} year < 1886 or year > Date.utc_today().year + 1 -> {:error, :invalid_car_year}
use_type not in @valid_use_types -> {:error, :invalid_use_type} use_type not in @valid_use_types -> {:error, :invalid_use_type}
car_type not in @valid_car_types -> {:error, :invalid_car_type} car_type not in @valid_car_types -> {:error, :invalid_car_type}
byte_size(chassis) == 0 -> {:error, :missing_chassis_number} byte_size(plate) == 0 -> {:error, :missing_plate}
byte_size(engine) == 0 -> {:error, :missing_engine_number}
true -> :ok true -> :ok
end end
end end
def validate_details(_), do: {:error, :invalid_car_details} def validate_insured_object(_), do: {:error, :invalid_car_details}
end 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 defmodule PolicyService.Aggregates.PolicyApplication do
@moduledoc """ @moduledoc """
Behaviour and __using__ macro for policy application aggregates. 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: Usage:
defmodule PolicyService.Aggregates.CarPolicyApplication do defmodule PolicyService.Aggregates.CarPolicyApplication do
@@ -10,7 +10,7 @@ defmodule PolicyService.Aggregates.PolicyApplication do
end end
""" """
@callback validate_details(map()) :: :ok | {:error, term()} @callback validate_insured_object(map()) :: :ok | {:error, term()}
defmacro __using__(opts) do defmacro __using__(opts) do
policy_type = Keyword.fetch!(opts, :policy_type) policy_type = Keyword.fetch!(opts, :policy_type)
@@ -30,7 +30,7 @@ defmodule PolicyService.Aggregates.PolicyApplication do
ProviderQuoteReceived, ProviderQuoteReceived,
AllQuotesReceived, AllQuotesReceived,
QuoteAccepted, QuoteAccepted,
SolicitationSent, SolicitationRequestSent,
PolicyIssued PolicyIssued
} }
@@ -39,29 +39,29 @@ defmodule PolicyService.Aggregates.PolicyApplication do
defstruct [ defstruct [
:id, :id,
:submitted_by, :submitted_by,
:applicant_info, :insured,
:policy_details, :buyer,
:insured_object,
:selected_providers, :selected_providers,
:accepted_quote_id,
:accepted_plan_id, :accepted_plan_id,
:accepted_provider_id, :accepted_by,
:solicitation_id, :provider_policy_number,
:policy_number,
:effective_date, :effective_date,
:expiry_date, :expiry_date,
:state, :state,
quotes: %{}, quotes: %{},
pending_endorsements: %{} endorsements: %{}
] ]
# ── Execute ──────────────────────────────────────────────────────────── # ── Execute ──────────────────────────────────────────────────
@impl Commanded.Aggregates.Aggregate @impl Commanded.Aggregates.Aggregate
def execute(%__MODULE__{state: nil}, %SubmitPolicyApplication{} = cmd) do def execute(%__MODULE__{state: nil}, %SubmitPolicyApplication{} = cmd) do
with :ok <- PolicyService.Aggregates.PolicyApplication.validate_policy_id(cmd.id), with :ok <- PolicyService.Aggregates.PolicyApplication.validate_policy_id(cmd.id),
:ok <- validate_insured(cmd.insured),
:ok <- :ok <-
PolicyService.Aggregates.PolicyApplication.validate_applicant(cmd.applicant_info), PolicyService.Aggregates.PolicyApplication.validate_buyer(cmd.buyer),
:ok <- validate_details(cmd.policy_details), :ok <- validate_insured_object(cmd.insured_object),
:ok <- :ok <-
PolicyService.Aggregates.PolicyApplication.validate_providers( PolicyService.Aggregates.PolicyApplication.validate_providers(
cmd.selected_providers cmd.selected_providers
@@ -72,8 +72,9 @@ defmodule PolicyService.Aggregates.PolicyApplication do
id: cmd.id, id: cmd.id,
provider_id: provider.provider_id, provider_id: provider.provider_id,
provider_email: provider.email, provider_email: provider.email,
applicant_info: cmd.applicant_info, insured: cmd.insured,
policy_details: cmd.policy_details, buyer: cmd.buyer,
insured_object: cmd.insured_object,
requested_at: DateTime.utc_now() requested_at: DateTime.utc_now()
} }
end) end)
@@ -82,8 +83,9 @@ defmodule PolicyService.Aggregates.PolicyApplication do
%PolicyApplicationSubmitted{ %PolicyApplicationSubmitted{
id: cmd.id, id: cmd.id,
submitted_by: cmd.submitted_by, submitted_by: cmd.submitted_by,
applicant_info: cmd.applicant_info, insured: cmd.insured,
policy_details: cmd.policy_details, buyer: cmd.buyer,
insured_object: cmd.insured_object,
selected_providers: cmd.selected_providers, selected_providers: cmd.selected_providers,
submitted_at: DateTime.utc_now() submitted_at: DateTime.utc_now()
} }
@@ -140,19 +142,30 @@ defmodule PolicyService.Aggregates.PolicyApplication do
end end
def execute(%__MODULE__{} = agg, %AcceptQuoteAndSolicit{} = cmd) do def execute(%__MODULE__{} = agg, %AcceptQuoteAndSolicit{} = cmd) do
with {:ok, quote} <- case Enum.find_value(agg.quotes, fn {provider_id, quote} ->
PolicyService.Aggregates.PolicyApplication.find_quote(agg, cmd.quote_id), case Enum.find(quote.plans, &(&1.plan_id == cmd.accepted_plan_id)) do
{:ok, provider} <- nil -> nil
PolicyService.Aggregates.PolicyApplication.find_provider(agg, quote.provider_id), plan -> %{quote: quote, provider: provider_id, plan: plan}
{:ok, plan} <- end
PolicyService.Aggregates.PolicyApplication.find_plan(quote, cmd.plan_id) do end) do
nil ->
{:error, :plan_not_found}
result ->
[
%QuoteAccepted{ %QuoteAccepted{
id: agg.id, id: agg.id,
quote: quote, quote: result.quote,
plan: plan, plan: result.plan,
provider: provider, provider: result.provider,
accepted_at: DateTime.utc_now() accepted_by: cmd.accepted_by
},
%SolicitationRequestSent{
id: agg.id,
plan: result.plan,
provider_id: result.provider
} }
]
end end
end end
@@ -162,14 +175,14 @@ defmodule PolicyService.Aggregates.PolicyApplication do
def execute(%__MODULE__{} = agg, %RecordPolicyIssued{} = cmd) do def execute(%__MODULE__{} = agg, %RecordPolicyIssued{} = cmd) do
%PolicyIssued{ %PolicyIssued{
id: agg.id, id: agg.id,
policy_number: cmd.policy_number, provider_policy_number: cmd.provider_policy_number,
effective_date: cmd.effective_date, effective_date: cmd.effective_date,
expiry_date: cmd.expiry_date, expiry_date: cmd.expiry_date,
issued_at: cmd.issued_at || DateTime.utc_now() issued_at: cmd.issued_at || DateTime.utc_now()
} }
end end
# ── Apply ────────────────────────────────────────────────────────────── # ── Apply ──────────────────────────────────────────────────────
@impl Commanded.Aggregates.Aggregate @impl Commanded.Aggregates.Aggregate
def apply(%__MODULE__{} = agg, %PolicyApplicationSubmitted{} = e) do def apply(%__MODULE__{} = agg, %PolicyApplicationSubmitted{} = e) do
@@ -177,8 +190,9 @@ defmodule PolicyService.Aggregates.PolicyApplication do
agg agg
| id: e.id, | id: e.id,
submitted_by: e.submitted_by, submitted_by: e.submitted_by,
applicant_info: e.applicant_info, insured: e.insured,
policy_details: e.policy_details, buyer: e.buyer,
insured_object: e.insured_object,
selected_providers: e.selected_providers, selected_providers: e.selected_providers,
quotes: %{}, quotes: %{},
state: :awaiting_quotes state: :awaiting_quotes
@@ -205,44 +219,41 @@ defmodule PolicyService.Aggregates.PolicyApplication do
def apply(%__MODULE__{} = agg, %QuoteAccepted{} = e) do def apply(%__MODULE__{} = agg, %QuoteAccepted{} = e) do
%__MODULE__{ %__MODULE__{
agg agg
| accepted_quote_id: e.quote.quote_id, | accepted_plan_id: e.plan.plan_id,
accepted_plan_id: e.plan.plan_id, accepted_by: e.accepted_by
accepted_provider_id: e.provider.provider_id,
state: :solicitation_sent
} }
end end
def apply(%__MODULE__{} = agg, %SolicitationSent{} = e) do def apply(%__MODULE__{} = agg, %SolicitationRequestSent{} = _e) do
%__MODULE__{agg | solicitation_id: e.solicitation_id} %__MODULE__{
agg
| state: :awaiting_policy
}
end end
def apply(%__MODULE__{} = agg, %PolicyIssued{} = e) do def apply(%__MODULE__{} = agg, %PolicyIssued{} = e) do
%__MODULE__{ %__MODULE__{
agg agg
| policy_number: e.policy_number, | provider_policy_number: e.provider_policy_number,
effective_date: e.effective_date, effective_date: e.effective_date,
expiry_date: e.expiry_date, expiry_date: e.expiry_date,
state: :issued state: :issued
} }
end end
# allow each aggregate to override any callback # ── Validation ───────────────────────────────────────────────────
defoverridable execute: 2, apply: 2
end
end
def validate_policy_id(%PolicyService.Aggregates.PolicyId{policy_type: _}), do: :ok def validate_insured(%{
def validate_policy_id(_), do: {:error, :invalid_policy_id_format} "type" => "individual",
"name" => n,
def validate_user(id) when is_binary(id) and byte_size(id) > 0, do: :ok "date_of_birth" => _,
def validate_user(_), do: {:error, :missing_user_id} "document_id" => d
})
def validate_applicant(%{"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, when is_binary(n) and is_binary(d) and byte_size(n) > 0 and byte_size(d) > 0,
do: :ok do: :ok
# Match on string keys for Company def validate_insured(%{
def validate_applicant(%{ "type" => "corporate",
"company_name" => c, "company_name" => c,
"ruc" => r, "ruc" => r,
"legal_rep_name" => rep, "legal_rep_name" => rep,
@@ -252,7 +263,40 @@ defmodule PolicyService.Aggregates.PolicyApplication do
byte_size(c) > 0 and byte_size(r) > 0, byte_size(c) > 0 and byte_size(r) > 0,
do: :ok 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(p) when is_list(p) and length(p) > 0, do: :ok
def validate_providers(_), do: {:error, :no_providers_selected} def validate_providers(_), do: {:error, :no_providers_selected}

View File

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

View File

@@ -10,14 +10,19 @@ defmodule PolicyService.Application do
children = [ children = [
PolicyService.CommandedApp, PolicyService.CommandedApp,
PolicyService.Handlers.QuoteRequestHandler, PolicyService.Handlers.QuoteRequestHandler,
PolicyService.Consumers.QuoteReceivedConsumer,
PolicyService.Projectors.PolicyProjector,
PolicyService.Consumers.PolicyIssuedConsumer,
PolicyService.Handlers.SolicitationRequestHandler, PolicyService.Handlers.SolicitationRequestHandler,
PolicyServiceWeb.Telemetry, PolicyService.Consumers.QuoteTaskConsumer,
PolicyService.Consumers.SolicitationTaskConsumer,
PolicyService.Projectors.PolicyProjector,
PolicyService.Repo, PolicyService.Repo,
{DNSCluster, query: Application.get_env(:policy_service, :dns_cluster_query) || :ignore}, {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 PolicyServiceWeb.Endpoint
] ]

View File

@@ -13,15 +13,39 @@ defmodule PolicyService.Router do
identity: :id identity: :id
) )
# Route Fire commands to Fire Aggregate # Route Life commands to Life Aggregate
dispatch( dispatch(
[ [
PolicyService.Commands.FirePolicy.SubmitPolicyApplication, PolicyService.Commands.LifePolicy.SubmitPolicyApplication,
PolicyService.Commands.FirePolicy.RecordProviderQuote, PolicyService.Commands.LifePolicy.RecordProviderQuote,
PolicyService.Commands.FirePolicy.AcceptQuoteAndSolicit, PolicyService.Commands.LifePolicy.AcceptQuoteAndSolicit,
PolicyService.Commands.FirePolicy.RecordPolicyIssued 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 identity: :id
) )
end end

View File

@@ -1,8 +1,8 @@
defmodule PolicyService.Commands.CarPolicy do 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(PolicyService.Commands.Policy.RecordProviderQuote)
defmodule RecordProviderQuote, do: use(Policy.RecordProviderQuote) defmodule AcceptQuoteAndSolicit, do: use(PolicyService.Commands.Policy.AcceptQuoteAndSolicit)
defmodule AcceptQuoteAndSolicit, do: use(Policy.AcceptQuoteAndSolicit) defmodule RecordPolicyIssued, do: use(PolicyService.Commands.Policy.RecordPolicyIssued)
defmodule RecordPolicyIssued, do: use(Policy.RecordPolicyIssued)
end 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 defmodule PolicyService.Commands.Policy do
@moduledoc """ @moduledoc """
Base templates for Policy commands. Base templates for Policy commands using TypedStruct.
Use these macros to ensure all policy types share the same structure.
""" """
defmodule SubmitPolicyApplication do defmodule SubmitPolicyApplication do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
defstruct [ use TypedStruct
:id,
:submitted_by, typedstruct do
:applicant_info, field :id, PolicyService.Aggregates.PolicyId.t(), enforce: true
:policy_details, field :submitted_by, String.t(), enforce: true
:selected_providers field :insured, map(), enforce: true
] field :buyer, map(), enforce: true
field :insured_object, map()
field :selected_providers, list(), enforce: true
end
end end
end end
end end
@@ -21,16 +23,18 @@ defmodule PolicyService.Commands.Policy do
defmodule RecordProviderQuote do defmodule RecordProviderQuote do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
defstruct [ use TypedStruct
:id,
:recorded_by, typedstruct do
:provider_id, field :id, PolicyService.Aggregates.PolicyId.t(), enforce: true
:quote_id, field :recorded_by, String.t(), enforce: true
:premium, field :provider_id, String.t(), enforce: true
:coverage_details, field :quote_id, String.t(), enforce: true
:valid_until, field :premium, String.t()
:plans field :coverage_details, map()
] field :valid_until, String.t(), enforce: true
field :plans, list(), enforce: true
end
end end
end end
end end
@@ -38,13 +42,13 @@ defmodule PolicyService.Commands.Policy do
defmodule AcceptQuoteAndSolicit do defmodule AcceptQuoteAndSolicit do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
defstruct [ use TypedStruct
:id,
:accepted_by, typedstruct do
:quote_id, field :id, PolicyService.Aggregates.PolicyId.t(), enforce: true
:plan_id, field :accepted_by, String.t()
:solicitation_fields field :accepted_plan_id, String.t(), enforce: true
] end
end end
end end
end end
@@ -52,13 +56,15 @@ defmodule PolicyService.Commands.Policy do
defmodule RecordPolicyIssued do defmodule RecordPolicyIssued do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
defstruct [ use TypedStruct
:id,
:policy_number, typedstruct do
:effective_date, field :id, PolicyService.Aggregates.PolicyId.t(), enforce: true
:expiry_date, field :provider_policy_number, String.t(), enforce: true
:issued_at field :effective_date, String.t(), enforce: true
] field :expiry_date, String.t(), enforce: true
field :issued_at, String.t()
end
end end
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 defmodule PolicyService.Events.Policy do
@moduledoc """
Policy domain events.
"""
defmodule PolicyApplicationSubmitted do defmodule PolicyApplicationSubmitted do
use PolicyService.Events
@derive Jason.Encoder @derive Jason.Encoder
defstruct [ defstruct [
:id, :id,
:submitted_by, :submitted_by,
:applicant_info, :insured,
:policy_details, :buyer,
:insured_object,
:selected_providers, :selected_providers,
:submitted_at :submitted_at
] ]
end end
defmodule QuoteRequestSent do defmodule QuoteRequestSent do
use PolicyService.Events
@derive Jason.Encoder @derive Jason.Encoder
defstruct [ defstruct [
:id, :id,
:provider_id, :provider_id,
:provider_email, :provider_email,
:applicant_info, :insured,
:policy_details, :buyer,
:insured_object,
:requested_at :requested_at
] ]
end end
defmodule ProviderQuoteReceived do defmodule ProviderQuoteReceived do
use PolicyService.Events
@derive Jason.Encoder @derive Jason.Encoder
defstruct [ defstruct [
:id, :id,
@@ -39,42 +71,26 @@ defmodule PolicyService.Events.Policy do
end end
defmodule AllQuotesReceived do defmodule AllQuotesReceived do
use PolicyService.Events
@derive Jason.Encoder @derive Jason.Encoder
defstruct [:id, :org_id, :quote_count] defstruct [:id, :quote_count]
end end
defmodule QuoteAccepted do defmodule QuoteAccepted do
use PolicyService.Events
@derive Jason.Encoder @derive Jason.Encoder
defstruct [ defstruct [:id, :accepted_by, :quote, :plan, :provider]
:id,
:accepted_by,
:quote,
:plan,
:provider,
:accepted_at
]
end end
defmodule SolicitationSent do defmodule SolicitationRequestSent do
use PolicyService.Events
@derive Jason.Encoder @derive Jason.Encoder
defstruct [ defstruct [:id, :plan, :provider_id]
:id,
:solicitation_id,
:provider_id,
:template_id,
:s3_key,
:sent_at
]
end end
defmodule PolicyIssued do defmodule PolicyIssued do
use PolicyService.Events
@derive Jason.Encoder @derive Jason.Encoder
defstruct [ defstruct [:id, :provider_policy_number, :effective_date, :expiry_date, :issued_at]
:id,
:policy_number,
:effective_date,
:expiry_date,
:issued_at
]
end end
end end

View File

@@ -6,6 +6,10 @@ defmodule PolicyService.Handlers.QuoteRequestHandler do
alias PolicyService.Events.Policy.QuoteRequestSent alias PolicyService.Events.Policy.QuoteRequestSent
def handle(%QuoteRequestSent{} = e, _metadata) do 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
end end

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,25 +10,23 @@ defmodule PolicyService.Projectors.PolicyProjector do
ProviderQuoteReceived, ProviderQuoteReceived,
AllQuotesReceived, AllQuotesReceived,
QuoteAccepted, QuoteAccepted,
SolicitationSent, SolicitationRequestSent,
PolicyIssued PolicyIssued
} }
alias PolicyService.Projections.PolicyApplication alias PolicyService.Projections.PolicyApplication
alias PolicyService.Aggregates.PolicyId
import Ecto.Query import Ecto.Query
project(%PolicyApplicationSubmitted{} = e, _meta, fn multi -> 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{ Ecto.Multi.insert(multi, :policy_application, %PolicyApplication{
id: to_string(PolicyId.new(org_id, policy_type, application_id)), id: to_string(e.id),
application_id: application_id, application_id: e.id.application_id,
org_id: org_id, org_id: e.id.org_id,
submitted_by: e.submitted_by, submitted_by: e.submitted_by,
policy_type: policy_type, policy_type: e.id.policy_type,
applicant_info: atomize(e.applicant_info), insured: atomize(e.insured),
policy_details: atomize(e.policy_details), buyer: atomize(e.buyer),
insured_object: atomize(e.insured_object),
selected_providers: Enum.map(e.selected_providers, & &1["provider_id"]), selected_providers: Enum.map(e.selected_providers, & &1["provider_id"]),
quotes: %{}, quotes: %{},
status: "quote_requested", status: "quote_requested",
@@ -71,25 +69,20 @@ defmodule PolicyService.Projectors.PolicyProjector do
end) end)
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} -> |> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
Ecto.Changeset.change(p, Ecto.Changeset.change(p,
accepted_quote_id: e.quote.quote_id,
accepted_plan_id: e.plan.plan_id, accepted_plan_id: e.plan.plan_id,
accepted_provider_id: e.provider.id, accepted_by: e.accepted_by
accepted_at: parse_datetime(e.accepted_at),
status: "solicitation_sent"
) )
end) end)
end) end)
project(%SolicitationSent{} = e, _meta, fn multi -> project(%SolicitationRequestSent{} = e, _meta, fn multi ->
multi multi
|> Ecto.Multi.run(:fetch, fn repo, _ -> |> Ecto.Multi.run(:fetch, fn repo, _ ->
{:ok, repo.get!(PolicyApplication, to_string(e.id))} {:ok, repo.get!(PolicyApplication, to_string(e.id))}
end) end)
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} -> |> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
Ecto.Changeset.change(p, Ecto.Changeset.change(p,
solicitation_id: e.solicitation_id, status: "awaiting_policy"
solicitation_s3_key: e.s3_key,
solicitation_sent_at: parse_datetime(e.sent_at)
) )
end) end)
end) end)
@@ -101,7 +94,7 @@ defmodule PolicyService.Projectors.PolicyProjector do
end) end)
|> Ecto.Multi.update(:policy_application, fn %{fetch: p} -> |> Ecto.Multi.update(:policy_application, fn %{fetch: p} ->
Ecto.Changeset.change(p, Ecto.Changeset.change(p,
policy_number: e.policy_number, provider_policy_number: e.provider_policy_number,
effective_date: parse_date(e.effective_date), effective_date: parse_date(e.effective_date),
expiry_date: parse_date(e.expiry_date), expiry_date: parse_date(e.expiry_date),
issued_at: parse_datetime(e.issued_at), 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 defmodule PolicyServiceWeb.ApiSpec do
alias OpenApiSpex.{OpenApi, Info, Server} alias OpenApiSpex.{OpenApi, Info, Server, Components, SecurityScheme}
alias OpenApiSpex.{Info, OpenApi, Paths, Server} alias OpenApiSpex.{Info, OpenApi, Paths, Server}
alias PolicyServiceWeb.{Endpoint, Router} alias PolicyServiceWeb.{Endpoint, Router}
@behaviour OpenApi @behaviour OpenApi
@@ -16,7 +16,18 @@ defmodule PolicyServiceWeb.ApiSpec do
version: "1.0" version: "1.0"
}, },
# Populate the paths from a phoenix router # 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 # Discover request/response schemas from path specs
|> OpenApiSpex.resolve_schema_modules() |> OpenApiSpex.resolve_schema_modules()

View File

@@ -0,0 +1,15 @@
defmodule PolicyServiceWeb.HealthController do
use PolicyServiceWeb, :controller
def health(conn, _params) do
conn
|> put_status(:ok)
|> json(%{status: "ok"})
end
def ready(conn, _params) do
conn
|> put_status(:ok)
|> json(%{status: "ready"})
end
end

View File

@@ -7,29 +7,23 @@ defmodule PolicyServiceWeb.PolicyController do
alias PolicyService.Aggregates.PolicyId alias PolicyService.Aggregates.PolicyId
alias PolicyService.Commands.CarPolicy alias PolicyService.Commands.CarPolicy
alias PolicyService.Commands.LifePolicy
alias PolicyService.Commands.FireStructurePolicy
alias PolicyService.Commands.FireContentsPolicy
alias PolicyServiceWeb.Schemas.Policy, as: S alias PolicyServiceWeb.Schemas.Policy, as: S
alias PolicyServiceWeb.QueryHelpers
tags(["Policies"]) tags(["Policies"])
security([%{"bearerAuth" => []}]) security([%{"bearerAuth" => []}])
# ---------------------------------------------------------------------------
# GET /api/policies
# ---------------------------------------------------------------------------
operation(:index, operation(:index,
summary: "List policies", summary: "List policies",
parameters: [ parameters:
"page[number]": [in: :query, type: :integer, required: false], QueryHelpers.flop(
"page[size]": [in: :query, type: :integer, required: false], [:status, :policy_type, :search],
"filters[0][field]": [in: :query, type: :string, required: false], [:submitted_at, :policy_type, :status]
"filters[0][op]": [in: :query, type: :string, required: false], ),
"filters[0][value]": [in: :query, type: :string, required: false],
"filters[1][field]": [in: :query, type: :string, required: false],
"filters[1][op]": [in: :query, type: :string, required: false],
"filters[1][value]": [in: :query, type: :string, required: false],
"order_by[]": [in: :query, type: :string, required: false]
],
responses: [ responses: [
ok: {"Policy list", "application/json", S.PolicyListResponse}, ok: {"Policy list", "application/json", S.PolicyListResponse},
bad_request: {"Invalid params", "application/json", S.ErrorResponse} bad_request: {"Invalid params", "application/json", S.ErrorResponse}
@@ -37,7 +31,7 @@ defmodule PolicyServiceWeb.PolicyController do
) )
def index(conn, params) 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 case PolicyQueries.list_by_org(org_id, params) do
{:ok, {policies, meta}} -> {:ok, {policies, meta}} ->
@@ -69,7 +63,7 @@ defmodule PolicyServiceWeb.PolicyController do
) )
def show(conn, %{"application_id" => application_id}) 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 case PolicyQueries.get_by_application_id(org_id, application_id) do
{:ok, policy} -> {:ok, policy} ->
@@ -95,12 +89,13 @@ defmodule PolicyServiceWeb.PolicyController do
def create(conn, params) do def create(conn, params) do
application_id = Ecto.UUID.generate() application_id = Ecto.UUID.generate()
org_id = conn.assigns[:org_id] || "test" org_id = conn.private[PolicyServiceWeb.Plugs.ExtractOrganizationId]
submitted_by = conn.assigns[:user_id] || "test" submitted_by = conn.assigns[:user_id]
with {:ok, policy_type} <- parse_policy_type(params["policy_type"]), with {:ok, policy_type} <- parse_policy_type(params["policy_type"]),
{:ok, applicant_info} <- parse_applicant_info(params["applicant_info"]), {:ok, insured} <- parse_insured(params["insured"]),
{:ok, policy_details} <- parse_policy_details(policy_type, params["policy_details"]), {: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 {:ok, providers} <- parse_providers(params["selected_providers"]) do
command = command =
case policy_type do case policy_type do
@@ -108,8 +103,39 @@ defmodule PolicyServiceWeb.PolicyController do
%CarPolicy.SubmitPolicyApplication{ %CarPolicy.SubmitPolicyApplication{
id: PolicyId.new(org_id, policy_type, application_id), id: PolicyId.new(org_id, policy_type, application_id),
submitted_by: submitted_by, submitted_by: submitted_by,
applicant_info: applicant_info, insured: insured,
policy_details: policy_details, 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 selected_providers: providers
} }
end end
@@ -147,7 +173,7 @@ defmodule PolicyServiceWeb.PolicyController do
) )
def accept(conn, %{"application_id" => application_id} = params) 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 with {:ok, policy} <- PolicyQueries.get_by_application_id(org_id, application_id) do
command = command =
@@ -155,9 +181,29 @@ defmodule PolicyServiceWeb.PolicyController do
"car" -> "car" ->
%CarPolicy.AcceptQuoteAndSolicit{ %CarPolicy.AcceptQuoteAndSolicit{
id: PolicyId.new(org_id, policy.policy_type, application_id), id: PolicyId.new(org_id, policy.policy_type, application_id),
quote_id: params["quote_id"], accepted_by: params["accepted_by"] || "system",
plan_id: params["plan_id"], accepted_plan_id: params["accepted_plan_id"]
solicitation_fields: params["solicitation_fields"] || %{} }
"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 end
@@ -166,9 +212,6 @@ defmodule PolicyServiceWeb.PolicyController do
{:ok, updated} = PolicyQueries.get_by_application_id(org_id, application_id) {:ok, updated} = PolicyQueries.get_by_application_id(org_id, application_id)
conn |> put_status(:ok) |> json(%{data: policy_detail(updated)}) 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} -> {:error, :plan_not_found} ->
conn |> put_status(:not_found) |> json(%{error: "plan not found"}) conn |> put_status(:not_found) |> json(%{error: "plan not found"})
@@ -181,54 +224,6 @@ defmodule PolicyServiceWeb.PolicyController do
end end
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 # Serializers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -238,9 +233,10 @@ defmodule PolicyServiceWeb.PolicyController do
application_id: p.application_id, application_id: p.application_id,
policy_type: p.policy_type, policy_type: p.policy_type,
status: p.status, status: p.status,
applicant_info: p.applicant_info, insured: p.insured,
policy_details: p.policy_details, buyer: p.buyer,
policy_number: p.policy_number, insured_object: p.insured_object,
provider_policy_number: p.provider_policy_number,
submitted_at: p.submitted_at submitted_at: p.submitted_at
} }
end end
@@ -252,17 +248,14 @@ defmodule PolicyServiceWeb.PolicyController do
submitted_by: p.submitted_by, submitted_by: p.submitted_by,
policy_type: p.policy_type, policy_type: p.policy_type,
status: p.status, status: p.status,
applicant_info: p.applicant_info, insured: p.insured,
policy_details: p.policy_details, buyer: p.buyer,
insured_object: p.insured_object,
selected_providers: p.selected_providers, selected_providers: p.selected_providers,
quotes: p.quotes, quotes: p.quotes,
accepted_quote_id: p.accepted_quote_id,
accepted_plan_id: p.accepted_plan_id, accepted_plan_id: p.accepted_plan_id,
accepted_provider_id: p.accepted_provider_id, accepted_by: p.accepted_by,
accepted_at: p.accepted_at, provider_policy_number: p.provider_policy_number,
solicitation_id: p.solicitation_id,
solicitation_s3_key: p.solicitation_s3_key,
policy_number: p.policy_number,
premium: p.premium, premium: p.premium,
effective_date: p.effective_date, effective_date: p.effective_date,
expiry_date: p.expiry_date, expiry_date: p.expiry_date,
@@ -287,12 +280,15 @@ defmodule PolicyServiceWeb.PolicyController do
# Parse helpers # 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} defp parse_policy_type(_), do: {:error, :invalid_policy_type}
# individual — has document_id # insured — individual
defp parse_applicant_info(%{"document_id" => doc} = info) defp parse_insured(info) do
when is_binary(doc) and byte_size(doc) > 0 do case info["type"] do
"individual" ->
case info["date_of_birth"] do case info["date_of_birth"] do
nil -> nil ->
{:error, :missing_date_of_birth} {:error, :missing_date_of_birth}
@@ -300,68 +296,145 @@ defmodule PolicyServiceWeb.PolicyController do
dob -> dob ->
{:ok, {:ok,
%{ %{
"type" => "individual",
"name" => info["name"], "name" => info["name"],
"date_of_birth" => dob, "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
end
# corporate — has ruc "corporate" ->
defp parse_applicant_info(%{"ruc" => ruc} = info)
when is_binary(ruc) and byte_size(ruc) > 0 do
{:ok, {:ok,
%{ %{
"type" => "corporate",
"company_name" => info["company_name"], "company_name" => info["company_name"],
"ruc" => ruc, "ruc" => info["ruc"],
"legal_rep_name" => info["legal_rep_name"], "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 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 # 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, {:ok,
%{ %{
"plate" => d["plate"], "plate" => d["plate"],
"make" => d["make"], "make" => d["make"],
"model" => d["model"], "model" => d["model"],
"year" => d["year"], "year" => d["year"],
"car_value" => d["car_value"],
"use_type" => d["use_type"], "use_type" => d["use_type"],
"car_type" => d["car_type"], "car_type" => d["car_type"],
"chassis_number" => d["chassis_number"], "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 end
# life details # 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, {:ok,
%{ %{
"coverage_type" => d["coverage_type"],
"coverage_amount" => d["coverage_amount"], "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 end
# fire details # fire_structure details
defp parse_policy_details("fire", nil), do: {:error, :missing_policy_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, {:ok,
%{ %{
"property_address" => d["property_address"], "location" => d["location"],
"property_value" => d["property_value"] "property_value" => d["property_value"],
"property_use" => d["property_use"],
"security_measures" => d["security_measures"] || [],
"market_value" => d["market_value"]
}} }}
end 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(nil), do: {:error, :missing_providers}
defp parse_providers([]), do: {:error, :no_providers_selected} defp parse_providers([]), do: {:error, :no_providers_selected}
@@ -371,8 +444,4 @@ defmodule PolicyServiceWeb.PolicyController do
end end
defp parse_providers(_), do: {:error, :invalid_providers} 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 end

View File

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

@@ -2,28 +2,90 @@ defmodule PolicyServiceWeb.Router do
use PolicyServiceWeb, :router use PolicyServiceWeb, :router
alias PolicyServiceWeb.PolicyController alias PolicyServiceWeb.PolicyController
alias PolicyServiceWeb.HealthController
pipeline :api do pipeline :api do
plug OpenApiSpex.Plug.PutApiSpec, module: PolicyServiceWeb.ApiSpec plug OpenApiSpex.Plug.PutApiSpec, module: PolicyServiceWeb.ApiSpec
end end
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
scope "/swaggerui" do
get "/", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
end
scope "/api" do scope "/api" do
pipe_through [:api] pipe_through [:api]
get "/openapi", OpenApiSpex.Plug.RenderSpec, [] get "/openapi", OpenApiSpex.Plug.RenderSpec, []
scope "/v1" do scope "/v1" do
pipe_through [:auth]
scope "/" do
pipe_through [:read]
get "/policies", PolicyController, :index get "/policies", PolicyController, :index
get "/policies/:application_id", PolicyController, :show get "/policies/:application_id", PolicyController, :show
end
scope "/" do
pipe_through [:create_request]
post "/policies", PolicyController, :create post "/policies", PolicyController, :create
end
scope "/" do
pipe_through [:submit_solicitation]
post "/policies/:application_id/accept", PolicyController, :accept post "/policies/:application_id/accept", PolicyController, :accept
get "/policies/:application_id/solicitation-url", PolicyController, :solicitation_url end
end end
end end
if Mix.env() == :dev do def introspect(conn, _opts) do
scope "/swaggerui" do zitadel = Application.get_env(:policy_service, :zitadel)
get "/", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
end 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
end end

View File

@@ -18,8 +18,105 @@ defmodule PolicyServiceWeb.Schemas.Policy do
}) })
end 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 defmodule ApplicantIndividual do
@@ -66,29 +163,28 @@ defmodule PolicyServiceWeb.Schemas.Policy do
# Policy details — one per policy type # Policy details — one per policy type
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
defmodule CarPolicyDetails do defmodule CarInsuredObject do
require OpenApiSpex require OpenApiSpex
OpenApiSpex.schema(%{ OpenApiSpex.schema(%{
title: "CarPolicyDetails", title: "CarInsuredObject",
type: :object, type: :object,
required: [ required: [
:plate, :plate,
:make, :make,
:model, :model,
:year, :year,
:car_value,
:use_type, :use_type,
:car_type, :car_type,
:chassis_number, :rc_limits,
:engine_number :market_value,
:requested_value
], ],
properties: %{ properties: %{
plate: %Schema{type: :string, example: "ABC-1234"}, plate: %Schema{type: :string, example: "ABC-1234"},
make: %Schema{type: :string, example: "Toyota"}, make: %Schema{type: :string, example: "Toyota"},
model: %Schema{type: :string, example: "Corolla"}, model: %Schema{type: :string, example: "Corolla"},
year: %Schema{type: :integer, example: 2022}, year: %Schema{type: :integer, example: 2022},
car_value: %Schema{type: :number, example: 18000},
use_type: %Schema{type: :string, enum: ["private", "commercial", "bus", "taxi", "school"]}, use_type: %Schema{type: :string, enum: ["private", "commercial", "bus", "taxi", "school"]},
car_type: %Schema{ car_type: %Schema{
type: :string, type: :string,
@@ -105,45 +201,106 @@ defmodule PolicyServiceWeb.Schemas.Policy do
] ]
}, },
chassis_number: %Schema{type: :string, example: "9BWZZZ377VT004251"}, 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 end
defmodule LifePolicyDetails do defmodule LifeInsuredObject do
require OpenApiSpex require OpenApiSpex
OpenApiSpex.schema(%{ OpenApiSpex.schema(%{
title: "LifePolicyDetails", title: "LifeInsuredObject",
type: :object, type: :object,
required: [:coverage_amount, :beneficiary], required: [:coverage_type, :coverage_amount, :coverage_years, :smoker],
properties: %{ properties: %{
coverage_type: %Schema{
type: :string,
enum: ["banking", "protection"]
},
coverage_amount: %Schema{type: :number, example: 100_000}, 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 end
defmodule FirePolicyDetails do defmodule FireStructureInsuredObject do
require OpenApiSpex require OpenApiSpex
OpenApiSpex.schema(%{ OpenApiSpex.schema(%{
title: "FirePolicyDetails", title: "FireStructureInsuredObject",
type: :object, type: :object,
required: [:property_address, :property_value], required: [:location, :property_value, :property_use, :security_measures, :market_value],
properties: %{ properties: %{
property_address: %Schema{type: :string, example: "Calle 50, Panama City"}, location: %Schema{type: :string, example: "Calle 50, Panama City"},
property_value: %Schema{type: :number, example: 250_000} 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 end
defmodule PolicyDetails do defmodule FireContentsInsuredObject do
require OpenApiSpex require OpenApiSpex
OpenApiSpex.schema(%{ OpenApiSpex.schema(%{
title: "PolicyDetails", title: "FireContentsInsuredObject",
oneOf: [CarPolicyDetails, LifePolicyDetails, FirePolicyDetails] 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 end
@@ -207,15 +364,16 @@ defmodule PolicyServiceWeb.Schemas.Policy do
OpenApiSpex.schema(%{ OpenApiSpex.schema(%{
title: "CreatePolicyRequest", title: "CreatePolicyRequest",
type: :object, type: :object,
required: [:policy_type, :applicant_info, :policy_details, :selected_providers], required: [:policy_type, :insured, :buyer, :insured_object, :selected_providers],
properties: %{ properties: %{
policy_type: %Schema{ policy_type: %Schema{
type: :string, type: :string,
enum: ["car", "life", "fire"], enum: ["car", "life", "fire_structure", "fire_contents"],
description: "Determines the shape of policy_details" description: "Determines the shape of insured_object"
}, },
applicant_info: ApplicantInfo, insured: Insured,
policy_details: PolicyDetails, buyer: Buyer,
insured_object: InsuredObject,
selected_providers: %Schema{type: :array, items: SelectedProvider, minItems: 1} selected_providers: %Schema{type: :array, items: SelectedProvider, minItems: 1}
} }
}) })
@@ -227,15 +385,11 @@ defmodule PolicyServiceWeb.Schemas.Policy do
OpenApiSpex.schema(%{ OpenApiSpex.schema(%{
title: "AcceptQuoteRequest", title: "AcceptQuoteRequest",
type: :object, type: :object,
required: [:quote_id, :plan_id], required: [:accepted_plan_id],
properties: %{ properties: %{
quote_id: %Schema{type: :string}, accepted_plan_id: %Schema{
plan_id: %Schema{type: :string}, type: :string,
solicitation_fields: %Schema{ description: "Plan ID to accept"
type: :object,
additionalProperties: %Schema{type: :string},
description: "Optional flat map of AcroForm field names to values",
nullable: true
} }
} }
}) })
@@ -258,20 +412,6 @@ defmodule PolicyServiceWeb.Schemas.Policy do
}) })
end 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 defmodule PolicySummary do
require OpenApiSpex require OpenApiSpex
@@ -280,14 +420,18 @@ defmodule PolicyServiceWeb.Schemas.Policy do
type: :object, type: :object,
properties: %{ properties: %{
application_id: %Schema{type: :string}, 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{ status: %Schema{
type: :string, type: :string,
enum: ["quote_requested", "quotes_received", "solicitation_sent", "issued"] enum: ["quote_requested", "quotes_received", "awaiting_policy", "issued"]
}, },
applicant_info: ApplicantInfo, insured: Insured,
policy_details: PolicyDetails, buyer: Buyer,
policy_number: %Schema{type: :string, nullable: true}, insured_object: InsuredObject,
provider_policy_number: %Schema{type: :string, nullable: true},
submitted_at: %Schema{type: :string, format: :"date-time"} submitted_at: %Schema{type: :string, format: :"date-time"}
} }
}) })
@@ -303,22 +447,22 @@ defmodule PolicyServiceWeb.Schemas.Policy do
application_id: %Schema{type: :string}, application_id: %Schema{type: :string},
org_id: %Schema{type: :string}, org_id: %Schema{type: :string},
submitted_by: %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{ status: %Schema{
type: :string, type: :string,
enum: ["quote_requested", "quotes_received", "solicitation_sent", "issued"] enum: ["quote_requested", "quotes_received", "awaiting_policy", "issued"]
}, },
applicant_info: ApplicantInfo, insured: Insured,
policy_details: PolicyDetails, buyer: Buyer,
insured_object: InsuredObject,
selected_providers: %Schema{type: :array, items: %Schema{type: :string}}, selected_providers: %Schema{type: :array, items: %Schema{type: :string}},
quotes: %Schema{type: :object, additionalProperties: QuoteData}, quotes: %Schema{type: :object, additionalProperties: QuoteData},
accepted_quote_id: %Schema{type: :string, nullable: true},
accepted_plan_id: %Schema{type: :string, nullable: true}, accepted_plan_id: %Schema{type: :string, nullable: true},
accepted_provider_id: %Schema{type: :string, nullable: true}, accepted_by: %Schema{type: :string, nullable: true},
accepted_at: %Schema{type: :string, format: :"date-time", nullable: true}, provider_policy_number: %Schema{type: :string, nullable: true},
solicitation_id: %Schema{type: :string, nullable: true},
solicitation_s3_key: %Schema{type: :string, nullable: true},
policy_number: %Schema{type: :string, nullable: true},
premium: %Schema{type: :number, nullable: true}, premium: %Schema{type: :number, nullable: true},
effective_date: %Schema{type: :string, format: :date, nullable: true}, effective_date: %Schema{type: :string, format: :date, nullable: true},
expiry_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, start_permanent: Mix.env() == :prod,
aliases: aliases(), aliases: aliases(),
deps: deps(), deps: deps(),
listeners: [Phoenix.CodeReloader] listeners: [Phoenix.CodeReloader],
dialyzer: [
plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
plt_add_apps: [:mix, :ex_unit]
]
] ]
end end
@@ -56,7 +60,11 @@ defmodule PolicyService.MixProject do
{:open_api_spex, "~> 3.20"}, {:open_api_spex, "~> 3.20"},
{:cors_plug, "~> 3.0"}, {:cors_plug, "~> 3.0"},
{:flop, "~> 0.26"}, {: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 end
@@ -72,7 +80,13 @@ defmodule PolicyService.MixProject do
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"], "ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], 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
end end

View File

@@ -3,18 +3,18 @@
"amqp_client": {:hex, :amqp_client, "4.2.1", "cff0cc13186e57457dc5745f1b3a4127c6857717cb8f5920dc457c84d0ad00a2", [:make, :rebar3], [{:credentials_obfuscation, "3.5.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:rabbit_common, "4.2.1", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm", "8ae00b055a58500e0557f73d9c0ffe257487131e603f7f84fe72cbfaaf03838a"}, "amqp_client": {:hex, :amqp_client, "4.2.1", "cff0cc13186e57457dc5745f1b3a4127c6857717cb8f5920dc457c84d0ad00a2", [:make, :rebar3], [{:credentials_obfuscation, "3.5.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:rabbit_common, "4.2.1", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm", "8ae00b055a58500e0557f73d9c0ffe257487131e603f7f84fe72cbfaaf03838a"},
"backoff": {:hex, :backoff, "1.1.6", "83b72ed2108ba1ee8f7d1c22e0b4a00cfe3593a67dbc792799e8cce9f42f796b", [:rebar3], [], "hexpm", "cf0cfff8995fb20562f822e5cc47d8ccf664c5ecdc26a684cbe85c225f9d7c39"}, "backoff": {:hex, :backoff, "1.1.6", "83b72ed2108ba1ee8f7d1c22e0b4a00cfe3593a67dbc792799e8cce9f42f796b", [:rebar3], [], "hexpm", "cf0cfff8995fb20562f822e5cc47d8ccf664c5ecdc26a684cbe85c225f9d7c39"},
"bandit": {:hex, :bandit, "1.10.2", "d15ea32eb853b5b42b965b24221eb045462b2ba9aff9a0bda71157c06338cbff", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "27b2a61b647914b1726c2ced3601473be5f7aa6bb468564a688646a689b3ee45"}, "bandit": {:hex, :bandit, "1.10.2", "d15ea32eb853b5b42b965b24221eb045462b2ba9aff9a0bda71157c06338cbff", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "27b2a61b647914b1726c2ced3601473be5f7aa6bb468564a688646a689b3ee45"},
"brod": {:hex, :brod, "3.19.1", "6e42e0b495108f8c691717654c6efef7a02f69d1eaaf885bb8d0f7aa8c04b9c7", [:rebar3], [{:kafka_protocol, "4.1.5", [hex: :kafka_protocol, repo: "hexpm", optional: false]}, {:snappyer, "1.2.9", [hex: :snappyer, repo: "hexpm", optional: false]}], "hexpm", "241899cff62e175cd60de4acd4b72f40edb3529b18853f8b22a8a35e4c76d71d"},
"commanded": {:hex, :commanded, "1.4.9", "289bc371943cf082f1161b1560563f5451ca176c967670cccd63fc3988fcd225", [:mix], [{:backoff, "~> 1.1", [hex: :backoff, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "a4f49c23041a23687aa10e99f3db7ee3b8ae470bb615b73b9f887b86437263e7"}, "commanded": {:hex, :commanded, "1.4.9", "289bc371943cf082f1161b1560563f5451ca176c967670cccd63fc3988fcd225", [:mix], [{:backoff, "~> 1.1", [hex: :backoff, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_registry, "~> 0.2", [hex: :telemetry_registry, repo: "hexpm", optional: false]}], "hexpm", "a4f49c23041a23687aa10e99f3db7ee3b8ae470bb615b73b9f887b86437263e7"},
"commanded_ecto_projections": {:hex, :commanded_ecto_projections, "1.4.0", "a1b220577577d5e0aee4c92b2d9bc6de221f9c1ac2ab36932cba15881761332f", [:mix], [{:commanded, "~> 1.4", [hex: :commanded, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8919a6173cd8f30fe2f948c2967f9289c7f5fe4eeca7abc67966bfca31f4aa9f"}, "commanded_ecto_projections": {:hex, :commanded_ecto_projections, "1.4.0", "a1b220577577d5e0aee4c92b2d9bc6de221f9c1ac2ab36932cba15881761332f", [:mix], [{:commanded, "~> 1.4", [hex: :commanded, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8919a6173cd8f30fe2f948c2967f9289c7f5fe4eeca7abc67966bfca31f4aa9f"},
"commanded_eventstore_adapter": {:hex, :commanded_eventstore_adapter, "1.4.2", "4f2d9d9bd8ef7807a5a4c278b4344adddbbbb4d9c86c693872bc85b944be1fe8", [:mix], [{:commanded, "~> 1.4", [hex: :commanded, repo: "hexpm", optional: false]}, {:eventstore, "~> 1.4", [hex: :eventstore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "26eaa68515e3e73834d769b73bddfea76c3fdcaff085d735c22b82a66ba19b10"}, "commanded_eventstore_adapter": {:hex, :commanded_eventstore_adapter, "1.4.2", "4f2d9d9bd8ef7807a5a4c278b4344adddbbbb4d9c86c693872bc85b944be1fe8", [:mix], [{:commanded, "~> 1.4", [hex: :commanded, repo: "hexpm", optional: false]}, {:eventstore, "~> 1.4", [hex: :eventstore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "26eaa68515e3e73834d769b73bddfea76c3fdcaff085d735c22b82a66ba19b10"},
"cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
"crc32cer": {:hex, :crc32cer, "0.1.8", "c6c2275c5fb60a95f4935d414f30b50ee9cfed494081c9b36ebb02edfc2f48db", [:rebar3], [], "hexpm", "251499085482920deb6c9b7aadabf9fb4c432f96add97ab42aee4501e5b6f591"},
"credentials_obfuscation": {:hex, :credentials_obfuscation, "3.5.0", "61e282adfb4439486b3994faaec69543c7ee6cc7e70c6340e8853fd9deaf8219", [:rebar3], [], "hexpm", "843adbe3246861ce0f1a0fa3222f384834eb31defd8d6b9cba7afd2977c957bc"}, "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"}, "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"}, "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"}, "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": {: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"}, "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"}, "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"}, "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"}, "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"},
@@ -23,11 +23,13 @@
"gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"}, "gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "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"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"kafka_protocol": {:hex, :kafka_protocol, "4.1.5", "d15e64994a8ca99716ab47db4132614359ac1bfa56d6c5b4341fdc1aa4041518", [:rebar3], [{:crc32cer, "0.1.8", [hex: :crc32cer, repo: "hexpm", optional: false]}], "hexpm", "c956c9357fef493b7072a35d0c3e2be02aa5186c804a412d29e62423bb15e5d9"}, "jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "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"}, "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_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "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"}, "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": {: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"}, "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"},
@@ -40,14 +42,13 @@
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
"snappyer": {:hex, :snappyer, "1.2.9", "9cc58470798648ce34c662ca0aa6daae31367667714c9a543384430a3586e5d3", [:rebar3], [], "hexpm", "18d00ca218ae613416e6eecafe1078db86342a66f86277bd45c95f05bf1c8b29"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
"telemetry_registry": {:hex, :telemetry_registry, "0.3.2", "701576890320be6428189bff963e865e8f23e0ff3615eade8f78662be0fc003c", [:mix, :rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7ed191eb1d115a3034af8e1e35e4e63d5348851d556646d46ca3d1b4e16bab9"}, "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"}, "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"}, "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, "typedstruct": {:hex, :typedstruct, "0.5.4", "d1d33d58460a74f413e9c26d55e66fd633abd8ac0fb12639add9a11a60a0462a", [:make, :mix], [], "hexpm", "ffaef36d5dbaebdbf4ed07f7fb2ebd1037b2c1f757db6fb8e7bcbbfabbe608d8"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "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"}, "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"},
} }

6
ops/chart/Chart.lock Normal file
View File

@@ -0,0 +1,6 @@
dependencies:
- name: common
repository: https://bjw-s-labs.github.io/helm-charts/
version: 4.6.2
digest: sha256:35e8f4e5d15d878c246a04eb51de580291f31203fa10e9e4d2318f16026b2061
generated: "2026-04-14T12:23:59.76714437-05:00"

View File

@@ -1,36 +0,0 @@
{{- /*
Policy Service PostgreSQL Cluster using CNPG
*/ -}}
{{- if .Values.postgresql.enabled -}}
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: {{ include "bjw-s.common.lib.chart.names.fullname" . }}-pg
namespace: {{ .Release.Namespace }}
labels:
{{- include "bjw-s.common.lib.chart.names.labels" . | nindent 4 }}
spec:
description: "PostgreSQL cluster for {{ .Release.Name }}"
imageName: {{ .Values.postgresql.image | default "ghcr.io/cloudnative-pg/container-image:1.23.1" }}
instances: {{ .Values.postgresql.instances | default 1 }}
bootstrap:
initdb:
database: {{ .Values.postgresql.database | default "policy_service" }}
owner: {{ .Values.postgresql.owner | default "policy_service" }}
storage:
storageClass: {{ .Values.postgresql.storageClass | default "local-path" }}
size: {{ .Values.postgresql.storageSize | default "1Gi" }}
resources:
requests:
cpu: {{ .Values.postgresql.resources.requests.cpu | default "100m" }}
memory: {{ .Values.postgresql.resources.requests.memory | default "128Mi" }}
limits:
cpu: {{ .Values.postgresql.resources.limits.cpu | default "500m" }}
memory: {{ .Values.postgresql.resources.limits.memory | default "512Mi" }}
monitoring:
enablePodMonitoring: true
{{- end -}}

View File

@@ -7,26 +7,90 @@ controllers:
migrate: migrate:
image: image:
repository: gitea.corredorconect.com/software-engineering/policy-service repository: gitea.corredorconect.com/software-engineering/policy-service
tag: latest tag: '{{ $.Chart.AppVersion }}'
pullPolicy: IfNotPresent
command: command:
- sh - "/bin/policy_service"
- -c args:
- > - "eval"
mix ecto.create && - "PolicyService.Release.migrate"
mix ecto.migrate &&
mix event_store.create &&
mix event_store.init
env: env:
MIX_ENV: prod 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: '{{ include "bjw-s.common.lib.chart.names.fullname" $ }}-cluster-pg-app'
key: uri
containers: containers:
main: main:
image: image:
repository: gitea.corredorconect.com/software-engineering/policy-service repository: gitea.corredorconect.com/software-engineering/policy-service
tag: latest tag: '{{ $.Chart.AppVersion }}'
pullPolicy: IfNotPresent
env: env:
LOG_LEVEL: debug
MIX_ENV: prod 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: '{{ 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: probes:
liveness: liveness:
enabled: true enabled: true
@@ -46,53 +110,175 @@ controllers:
port: 8080 port: 8080
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 5 periodSeconds: 5
resources: # resources:
requests: # requests:
cpu: 100m # cpu: 100m
memory: 256Mi # memory: 256Mi
limits: # limits:
cpu: 500m # cpu: 500m
memory: 512Mi # memory: 512Mi
service: service:
main: main:
enabled: true
controller: main controller: main
primary: true
type: ClusterIP type: ClusterIP
ports: ports:
http: http:
enabled: true
primary: true
port: 8080 port: 8080
protocol: HTTP protocol: HTTP
ingress: rawResources:
main: rabbitmq-user:
enabled: false
className: nginx
hosts:
- host: policy-service.local
paths:
- path: /
pathType: Prefix
service:
identifier: main
port: http
postgresql:
enabled: true enabled: true
image: ghcr.io/cloudnative-pg/container-image:1.23.1 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
kind: Cluster
suffix: pg
spec:
spec:
description: "PostgreSQL cluster for policy-service"
instances: 1 instances: 1
bootstrap:
initdb:
database: policy_service database: policy_service
owner: policy_service owner: policy_service
storageClass: local-path storage:
storageSize: 1Gi 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,31 +7,28 @@ defmodule PolicyService.Repo.Migrations.CreatePolicyApplications do
add :application_id, :string, null: false add :application_id, :string, null: false
add :org_id, :string, null: false add :org_id, :string, null: false
add :submitted_by, :string, null: false add :submitted_by, :string, null: false
add :policy_type, :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 # Insured — full map, shape varies by individual vs corporate
add :applicant_info, :map, default: %{} add :insured, :map, default: %{}
# Policy-type-specific details — shape varies by policy_type # Buyer — full map, shape varies by individual vs corporate
add :policy_details, :map, default: %{} add :buyer, :map, default: %{}
# Insured object — policy-type-specific details, shape varies by policy_type
add :insured_object, :map, default: %{}
# Providers + quotes # Providers + quotes
add :selected_providers, {:array, :string}, default: [] add :selected_providers, {:array, :string}, default: []
add :quotes, :map, default: %{} add :quotes, :map, default: %{}
# Accepted plan # Accepted plan
add :accepted_quote_id, :string
add :accepted_plan_id, :string add :accepted_plan_id, :string
add :accepted_provider_id, :string
add :accepted_by, :string add :accepted_by, :string
add :accepted_at, :utc_datetime_usec
# Solicitation
add :solicitation_id, :string
add :solicitation_s3_key, :string
# Issued policy # Issued policy
add :policy_number, :string add :provider_policy_number, :string
add :premium, :decimal add :premium, :decimal
add :effective_date, :date add :effective_date, :date
add :expiry_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