This commit is contained in:
Haim Kortovich
2026-03-05 11:30:08 -05:00
commit a52f049a29
46 changed files with 1938 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

5
.formatter.exs Normal file
View File

@@ -0,0 +1,5 @@
[
import_deps: [:ecto, :ecto_sql, :phoenix],
subdirectories: ["priv/*/migrations"],
inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}", "priv/*/seeds.exs"]
]

27
.gitignore vendored Normal file
View File

@@ -0,0 +1,27 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Temporary files, for example, from tests.
/tmp/
# Ignore package tarball (built via "mix hex.build").
policy_service-*.tar

111
AGENTS.md Normal file
View File

@@ -0,0 +1,111 @@
This is a web application written using the Phoenix web framework.
## Project guidelines
- Use `mix precommit` alias when you are done with all changes and fix any pending issues
- Use the already included and available `:req` (`Req`) library for HTTP requests, **avoid** `:httpoison`, `:tesla`, and `:httpc`. Req is included by default and is the preferred HTTP client for Phoenix apps
### Phoenix v1.8 guidelines
- **Always** begin your LiveView templates with `<Layouts.app flash={@flash} ...>` which wraps all inner content
- The `MyAppWeb.Layouts` module is aliased in the `my_app_web.ex` file, so you can use it without needing to alias it again
- Anytime you run into errors with no `current_scope` assign:
- You failed to follow the Authenticated Routes guidelines, or you failed to pass `current_scope` to `<Layouts.app>`
- **Always** fix the `current_scope` error by moving your routes to the proper `live_session` and ensure you pass `current_scope` as needed
- Phoenix v1.8 moved the `<.flash_group>` component to the `Layouts` module. You are **forbidden** from calling `<.flash_group>` outside of the `layouts.ex` module
- Out of the box, `core_components.ex` imports an `<.icon name="hero-x-mark" class="w-5 h-5"/>` component for for hero icons. **Always** use the `<.icon>` component for icons, **never** use `Heroicons` modules or similar
- **Always** use the imported `<.input>` component for form inputs from `core_components.ex` when available. `<.input>` is imported and using it will save steps and prevent errors
- If you override the default input classes (`<.input class="myclass px-2 py-1 rounded-lg">)`) class with your own values, no default classes are inherited, so your
custom classes must fully style the input
<!-- usage-rules-start -->
<!-- phoenix:elixir-start -->
## Elixir guidelines
- Elixir lists **do not support index based access via the access syntax**
**Never do this (invalid)**:
i = 0
mylist = ["blue", "green"]
mylist[i]
Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie:
i = 0
mylist = ["blue", "green"]
Enum.at(mylist, i)
- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc
you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie:
# INVALID: we are rebinding inside the `if` and the result never gets assigned
if connected?(socket) do
socket = assign(socket, :val, val)
end
# VALID: we rebind the result of the `if` to a new variable
socket =
if connected?(socket) do
assign(socket, :val, val)
end
- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets
- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package)
- Don't use `String.to_atom/1` on user input (memory leak risk)
- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards
- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)`
- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option
## Mix guidelines
- Read the docs and options before using tasks (by using `mix help task_name`)
- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed`
- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason
## Test guidelines
- **Always use `start_supervised!/1`** to start processes in tests as it guarantees cleanup between tests
- **Avoid** `Process.sleep/1` and `Process.alive?/1` in tests
- Instead of sleeping to wait for a process to finish, **always** use `Process.monitor/1` and assert on the DOWN message:
ref = Process.monitor(pid)
assert_receive {:DOWN, ^ref, :process, ^pid, :normal}
- Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages
<!-- phoenix:elixir-end -->
<!-- phoenix:phoenix-start -->
## Phoenix guidelines
- Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes.
- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie:
scope "/admin", AppWeb.Admin do
pipe_through :browser
live "/users", UserLive, :index
end
the UserLive route would point to the `AppWeb.Admin.UserLive` module
- `Phoenix.View` no longer is needed or included with Phoenix, don't use it
<!-- phoenix:phoenix-end -->
<!-- phoenix:ecto-start -->
## Ecto Guidelines
- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email`
- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs`
- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string`
- `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed
- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields
- Fields which are set programatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct
- **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied
<!-- phoenix:ecto-end -->
<!-- usage-rules-end -->

18
README.md Normal file
View File

@@ -0,0 +1,18 @@
# PolicyService
To start your Phoenix server:
* Run `mix setup` to install and setup dependencies
* Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server`
Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
## Learn more
* Official website: https://www.phoenixframework.org/
* Guides: https://hexdocs.pm/phoenix/overview.html
* Docs: https://hexdocs.pm/phoenix
* Forum: https://elixirforum.com/c/phoenix-forum
* Source: https://github.com/phoenixframework/phoenix

48
config/config.exs Normal file
View File

@@ -0,0 +1,48 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
# General application configuration
import Config
config :policy_service,
ecto_repos: [PolicyService.Repo],
event_stores: [PolicyService.EventStore]
config :policy_service, PolicyServiceWeb.Endpoint,
url: [host: "localhost"],
adapter: Bandit.PhoenixAdapter,
render_errors: [
formats: [json: PolicyServiceWeb.ErrorJSON],
layout: false
],
pubsub_server: PolicyService.PubSub,
live_view: [signing_salt: "zPStCmh9"]
# Configure Elixir's Logger
config :logger, :default_formatter,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id]
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason
config :policy_service, PolicyService.CommandedApp,
event_store: [
adapter: Commanded.EventStore.Adapters.EventStore,
event_store: PolicyService.EventStore
],
pub_sub: :local,
registry: :local
config :commanded,
event_store_adapter: Commanded.EventStore.Adapters.EventStore
config :commanded_ecto_projections,
repo: PolicyService.Repo
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"

75
config/dev.exs Normal file
View File

@@ -0,0 +1,75 @@
import Config
config :policy_service, amqp_url: "amqp://guest:guest@localhost:5672"
config :policy_service, PolicyService.EventStore,
serializer: Commanded.Serialization.JsonSerializer,
username: "postgres",
password: "postgres",
database: "policy_service_eventstore_dev",
hostname: "localhost",
pool_size: 10
# Configure your database
config :policy_service, PolicyService.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "policy_service_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,
pool_size: 10
# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we can use it
# to bundle .js and .css sources.
config :policy_service, PolicyServiceWeb.Endpoint,
# Binding to loopback ipv4 address prevents access from other machines.
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
http: [ip: {127, 0, 0, 1}],
check_origin: false,
code_reloader: true,
debug_errors: true,
secret_key_base: "rSPVNB6DCC2RMMlmk9QkCVGAzasUD6AWh5ussctvNuUxgZL9DRnFXTo6jcIz6JpB",
watchers: []
# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# Mix task:
#
# mix phx.gen.cert
#
# Run `mix help phx.gen.cert` for more information.
#
# The `http:` config above can be replaced with:
#
# https: [
# port: 4001,
# cipher_suite: :strong,
# keyfile: "priv/cert/selfsigned_key.pem",
# certfile: "priv/cert/selfsigned.pem"
# ],
#
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.
# Enable dev routes for dashboard and mailbox
config :policy_service, dev_routes: true
# Do not include metadata nor timestamps in development logs
config :logger, :default_formatter, format: "[$level] $message\n"
# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20
# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime
config :open_api_spex, :cache_adapter, OpenApiSpex.Plug.NoneCache

17
config/prod.exs Normal file
View File

@@ -0,0 +1,17 @@
import Config
# Force using SSL in production. This also sets the "strict-security-transport" header,
# 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
# of environment variables, is done on config/runtime.exs.

102
config/runtime.exs Normal file
View File

@@ -0,0 +1,102 @@
import Config
# config/runtime.exs is executed for all environments, including
# during releases. It is executed after compilation and before the
# system starts, so it is typically used to load production configuration
# and secrets from environment variables or elsewhere. Do not define
# any compile-time configuration in here, as it won't be applied.
# The block below contains prod specific runtime configuration.
# ## Using releases
#
# If you use `mix release`, you need to explicitly enable the server
# by passing the PHX_SERVER=true when you start it:
#
# PHX_SERVER=true bin/policy_service start
#
# Alternatively, you can use `mix phx.gen.release` to generate a `bin/server`
# script that automatically sets the env var above.
if System.get_env("PHX_SERVER") do
config :policy_service, PolicyServiceWeb.Endpoint, server: true
end
config :policy_service, PolicyServiceWeb.Endpoint,
http: [port: String.to_integer(System.get_env("PORT", "4000"))]
if config_env() == :prod do
database_url =
System.get_env("DATABASE_URL") ||
raise """
environment variable DATABASE_URL is missing.
For example: ecto://USER:PASS@HOST/DATABASE
"""
maybe_ipv6 = if System.get_env("ECTO_IPV6") in ~w(true 1), do: [:inet6], else: []
config :policy_service, PolicyService.Repo,
# ssl: true,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
# For machines with several cores, consider starting multiple pools of `pool_size`
# pool_count: 4,
socket_options: maybe_ipv6
# The secret key base is used to sign/encrypt cookies and other secrets.
# A default value is used in config/dev.exs and config/test.exs but you
# want to use a different value for prod and you most likely don't want
# to check this value into version control, so we use an environment
# variable instead.
secret_key_base =
System.get_env("SECRET_KEY_BASE") ||
raise """
environment variable SECRET_KEY_BASE is missing.
You can generate one by calling: mix phx.gen.secret
"""
host = System.get_env("PHX_HOST") || "example.com"
config :policy_service, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY")
config :policy_service, PolicyServiceWeb.Endpoint,
url: [host: host, port: 443, scheme: "https"],
http: [
# Enable IPv6 and bind on all interfaces.
# Set it to {0, 0, 0, 0, 0, 0, 0, 1} for local network only access.
# 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
# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to your endpoint configuration:
#
# config :policy_service, PolicyServiceWeb.Endpoint,
# https: [
# ...,
# port: 443,
# cipher_suite: :strong,
# 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

37
config/test.exs Normal file
View File

@@ -0,0 +1,37 @@
import Config
# Configure your database
#
# The MIX_TEST_PARTITION environment variable can be used
# to provide built-in test partitioning in CI environment.
# Run `mix help test` for more information.
config :policy_service, PolicyService.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "policy_service_test#{System.get_env("MIX_TEST_PARTITION")}",
pool: Ecto.Adapters.SQL.Sandbox,
pool_size: System.schedulers_online() * 2
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :policy_service, PolicyServiceWeb.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 4002],
secret_key_base: "mf3rysPsftCpSAdQfBIFEKFpjA1e9tGi9+jbxhNTs5qC3pC9LSn6P/kWlZatl9a0",
server: false
# Print only warnings and errors during test
config :logger, level: :warning
# Initialize plugs at runtime for faster test compilation
config :phoenix, :plug_init_mode, :runtime
# Sort query params output of verified routes for robust url comparisons
config :phoenix,
sort_verified_routes_query_params: true
config :policy_service, PolicyService.Application,
event_store: [
adapter: Commanded.EventStore.Adapters.InMemory,
serializer: Commanded.Serialization.JsonSerializer
]

22
docker-compose.yml Normal file
View File

@@ -0,0 +1,22 @@
services:
rabbitmq:
image: rabbitmq:3.13-management
container_name: policy_service_rabbitmq
ports:
- "5672:5672" # AMQP
- "15672:15672" # Management UI
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
volumes:
- rabbitmq_data:/var/lib/rabbitmq
- ./rabbitmq/definitions.json:/etc/rabbitmq/definitions.json
- ./rabbitmq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 10s
timeout: 5s
retries: 5
volumes:
rabbitmq_data:

60
flake.lock generated Normal file
View File

@@ -0,0 +1,60 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1770827874,
"narHash": "sha256-c46lN+QyhURJIGO2ZjpEHGjhAcQCEn+L0Er219ridNs=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "f4a37e804018a73d81c2bdc2643a64c944b57d92",
"type": "github"
},
"original": {
"owner": "nixos",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

25
flake.nix Normal file
View File

@@ -0,0 +1,25 @@
{
description = "test";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShell =
pkgs.mkShell {
buildInputs = with pkgs;
[
elixir
elixir-ls
git
];
};
});
}

9
lib/policy_service.ex Normal file
View File

@@ -0,0 +1,9 @@
defmodule PolicyService do
@moduledoc """
PolicyService keeps the contexts that define your domain
and business logic.
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
end

View File

@@ -0,0 +1,309 @@
defmodule PolicyService.Aggregates.CarPolicyApplication do
@moduledoc """
Aggregate for managing car insurance policy applications.
Lifecycle:
nil → :awaiting_quotes → :solicitation_sent → :issued
"""
defstruct [
:application_id,
:org_id,
:submitted_by,
:applicant_info,
:car_details,
:selected_providers,
:quotes,
:accepted_quote_id,
:accepted_provider_id,
:policy_number,
:state
]
alias PolicyService.Commands.Car.{
SubmitCarPolicyApplication,
RecordCarProviderQuote,
AcceptCarQuoteAndSolicit,
RecordCarPolicyIssued
}
alias PolicyService.Events.Car.{
CarPolicyApplicationSubmitted,
CarProviderQuoteReceived,
AllCarQuotesReceived,
CarQuoteAccepted,
CarSolicitationSent,
CarPolicyIssued,
CarQuoteRequestSent
}
# ---------------------------------------------------------------------------
# Submit — establishes org ownership
# ---------------------------------------------------------------------------
def execute(%__MODULE__{state: nil}, %SubmitCarPolicyApplication{} = cmd) do
with :ok <- validate_org(cmd.org_id),
:ok <- validate_user(cmd.submitted_by),
:ok <- validate_applicant(cmd.applicant_info),
:ok <- validate_car_details(cmd.car_details),
:ok <- validate_providers(cmd.selected_providers) do
quote_requests =
Enum.map(cmd.selected_providers, fn provider ->
%CarQuoteRequestSent{
application_id: cmd.application_id,
org_id: cmd.org_id,
provider_id: provider.id,
provider_email: provider.email,
applicant_info: cmd.applicant_info,
car_details: cmd.car_details,
requested_at: DateTime.utc_now()
}
end)
[
%CarPolicyApplicationSubmitted{
application_id: cmd.application_id,
org_id: cmd.org_id,
submitted_by: cmd.submitted_by,
applicant_info: cmd.applicant_info,
car_details: cmd.car_details,
selected_providers: cmd.selected_providers,
submitted_at: DateTime.utc_now()
}
| quote_requests
]
end
end
def execute(%__MODULE__{state: state}, %SubmitCarPolicyApplication{}) do
{:error, {:invalid_state, "cannot submit in state: #{state}"}}
end
# ---------------------------------------------------------------------------
# Record provider quote — external webhook, verify org
# ---------------------------------------------------------------------------
def execute(%__MODULE__{state: :awaiting_quotes} = agg, %RecordCarProviderQuote{} = cmd) do
with :ok <- verify_org(agg, cmd) do
if Map.has_key?(agg.quotes, cmd.provider_id) do
{:error, {:duplicate_quote, "quote from provider #{cmd.provider_id} already received"}}
else
quote_event = %CarProviderQuoteReceived{
application_id: cmd.application_id,
org_id: agg.org_id,
recorded_by: cmd.recorded_by,
provider_id: cmd.provider_id,
quote_id: cmd.quote_id,
premium: cmd.premium,
coverage_details: cmd.coverage_details,
valid_until: cmd.valid_until,
received_at: DateTime.utc_now()
}
new_quote_count = map_size(agg.quotes) + 1
if new_quote_count == length(agg.selected_providers) do
[
quote_event,
%AllCarQuotesReceived{
application_id: cmd.application_id,
org_id: agg.org_id,
quote_count: new_quote_count
}
]
else
quote_event
end
end
end
end
def execute(%__MODULE__{state: state}, %RecordCarProviderQuote{}) do
{:error, {:invalid_state, "cannot record quote in state: #{state}"}}
end
# ---------------------------------------------------------------------------
# Accept quote and solicit — internal user action, verify org
# ---------------------------------------------------------------------------
def execute(%__MODULE__{state: :awaiting_quotes} = agg, %AcceptCarQuoteAndSolicit{} = cmd) do
with :ok <- verify_org(agg, cmd) do
case find_quote(agg.quotes, cmd.quote_id) do
nil ->
{:error, {:quote_not_found, "quote #{cmd.quote_id} not found"}}
{provider_id, _quote} ->
[
%CarQuoteAccepted{
application_id: cmd.application_id,
org_id: agg.org_id,
accepted_by: cmd.accepted_by,
quote_id: cmd.quote_id,
provider_id: provider_id,
accepted_at: DateTime.utc_now()
},
%CarSolicitationSent{
application_id: cmd.application_id,
org_id: agg.org_id,
provider_id: provider_id,
quote_id: cmd.quote_id,
sent_at: DateTime.utc_now()
}
]
end
end
end
def execute(%__MODULE__{state: state}, %AcceptCarQuoteAndSolicit{}) do
{:error, {:invalid_state, "cannot accept quote in state: #{state}"}}
end
# ---------------------------------------------------------------------------
# Record policy issued — external or internal, verify org
# ---------------------------------------------------------------------------
def execute(%__MODULE__{state: :solicitation_sent} = agg, %RecordCarPolicyIssued{} = cmd) do
with :ok <- verify_org(agg, cmd) do
if cmd.provider_id != agg.accepted_provider_id do
{:error, {:provider_mismatch, "policy issued by unexpected provider"}}
else
%CarPolicyIssued{
application_id: cmd.application_id,
org_id: agg.org_id,
recorded_by: cmd.recorded_by,
policy_number: cmd.policy_number,
provider_id: cmd.provider_id,
effective_date: cmd.effective_date,
expiry_date: cmd.expiry_date,
issued_at: DateTime.utc_now()
}
end
end
end
def execute(%__MODULE__{state: state}, %RecordCarPolicyIssued{}) do
{:error, {:invalid_state, "cannot record policy in state: #{state}"}}
end
# ---------------------------------------------------------------------------
# Apply events
# ---------------------------------------------------------------------------
def apply(%__MODULE__{} = agg, %CarPolicyApplicationSubmitted{} = e) do
%__MODULE__{
agg
| application_id: e.application_id,
org_id: e.org_id,
submitted_by: e.submitted_by,
applicant_info: e.applicant_info,
car_details: e.car_details,
selected_providers: e.selected_providers,
quotes: %{},
state: :awaiting_quotes
}
end
def apply(%__MODULE__{} = agg, %CarQuoteRequestSent{}), do: agg
def apply(%__MODULE__{} = agg, %CarProviderQuoteReceived{} = e) do
quote_data = %{
quote_id: e.quote_id,
premium: e.premium,
coverage_details: e.coverage_details,
valid_until: e.valid_until
}
%__MODULE__{agg | quotes: Map.put(agg.quotes, e.provider_id, quote_data)}
end
def apply(%__MODULE__{} = agg, %AllCarQuotesReceived{}), do: agg
def apply(%__MODULE__{} = agg, %CarQuoteAccepted{} = e) do
%__MODULE__{agg | accepted_quote_id: e.quote_id, accepted_provider_id: e.provider_id}
end
def apply(%__MODULE__{} = agg, %CarSolicitationSent{}) do
%__MODULE__{agg | state: :solicitation_sent}
end
def apply(%__MODULE__{} = agg, %CarPolicyIssued{} = e) do
%__MODULE__{agg | policy_number: e.policy_number, state: :issued}
end
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
defp verify_org(%__MODULE__{org_id: org_id}, %{org_id: cmd_org_id}) do
if org_id == cmd_org_id,
do: :ok,
else: {:error, :org_mismatch}
end
defp validate_org(org_id) when is_binary(org_id) and byte_size(org_id) > 0, do: :ok
defp validate_org(_), do: {:error, :missing_org_id}
defp validate_user(user_id) when is_binary(user_id) and byte_size(user_id) > 0, do: :ok
defp validate_user(_), do: {:error, :missing_user_id}
defp validate_applicant(%{name: name, date_of_birth: dob, document_id: doc})
when is_binary(name) and is_binary(doc),
do: :ok
defp validate_applicant(_), do: {:error, :invalid_applicant_info}
@valid_use_types ~w(private commercial bus taxi school)a
@valid_car_types ~w(sedan suv hatchback coupe convertible pickup van minivan truck)a
defp validate_car_details(%{
plate: plate,
make: make,
model: model,
year: year,
car_value: car_value,
use_type: use_type,
car_type: car_type,
chassis_number: chassis_number,
engine_number: engine_number
})
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_binary(chassis_number) and is_binary(engine_number) do
current_year = Date.utc_today().year
cond do
year < 1886 ->
{:error, :invalid_car_year}
year > current_year + 1 ->
{:error, :invalid_car_year}
use_type not in @valid_use_types ->
{:error, {:invalid_use_type, "must be one of: #{inspect(@valid_use_types)}"}}
car_type not in @valid_car_types ->
{:error, {:invalid_car_type, "must be one of: #{inspect(@valid_car_types)}"}}
byte_size(chassis_number) == 0 ->
{:error, :missing_chassis_number}
byte_size(engine_number) == 0 ->
{:error, :missing_engine_number}
true ->
:ok
end
end
defp validate_car_details(_), do: {:error, :invalid_car_details}
defp validate_providers(providers)
when is_list(providers) and length(providers) > 0,
do: :ok
defp validate_providers(_), do: {:error, :no_providers_selected}
defp find_quote(quotes, quote_id) do
Enum.find(quotes, fn {_provider_id, q} -> q.quote_id == quote_id end)
end
end

View File

@@ -0,0 +1,33 @@
defmodule PolicyService.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
PolicyService.CommandedApp,
PolicyService.Handlers.QuoteRequestHandler,
PolicyServiceWeb.Telemetry,
PolicyService.Repo,
{DNSCluster, query: Application.get_env(:policy_service, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: PolicyService.PubSub},
PolicyServiceWeb.Endpoint
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: PolicyService.Supervisor]
Supervisor.start_link(children, opts)
end
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
@impl true
def config_change(changed, _new, removed) do
PolicyServiceWeb.Endpoint.config_change(changed, removed)
:ok
end
end

View File

@@ -0,0 +1,23 @@
defmodule PolicyService.Router do
use Commanded.Commands.Router
alias PolicyService.Commands.Car
alias PolicyService.Aggregates
dispatch(
[
Car.SubmitCarPolicyApplication,
Car.RecordCarProviderQuote,
Car.AcceptCarQuoteAndSolicit,
Car.RecordCarPolicyIssued
],
to: PolicyService.Aggregates.CarPolicyApplication,
identity: :application_id
)
end
defmodule PolicyService.CommandedApp do
use Commanded.Application,
otp_app: :policy_service
router(PolicyService.Router)
end

View File

@@ -0,0 +1,39 @@
defmodule PolicyService.Commands.Car.SubmitCarPolicyApplication do
defstruct [
:application_id,
:org_id,
:submitted_by,
:applicant_info,
:car_details,
:selected_providers
]
end
defmodule PolicyService.Commands.Car.RecordCarProviderQuote do
defstruct [
:application_id,
:org_id,
:recorded_by,
:provider_id,
:quote_id,
:premium,
:coverage_details,
:valid_until
]
end
defmodule PolicyService.Commands.Car.AcceptCarQuoteAndSolicit do
defstruct [:application_id, :org_id, :accepted_by, :quote_id]
end
defmodule PolicyService.Commands.Car.RecordCarPolicyIssued do
defstruct [
:application_id,
:org_id,
:recorded_by,
:policy_number,
:provider_id,
:effective_date,
:expiry_date
]
end

View File

@@ -0,0 +1,16 @@
defmodule PolicyService.Common.CarInfo do
use ExConstructor
@derive Jason.Encoder
defstruct [
:plate,
:make,
:model,
:year,
:car_value,
:use_type,
:car_type,
:chassis_number,
:engine_number
]
end

View File

@@ -0,0 +1,6 @@
defmodule PolicyService.Common.ClientInfo do
use ExConstructor
@derive Jason.Encoder
defstruct [:first_name, :last_name, :birth_date, :gender, :email, :phone, :user_id]
end

View File

@@ -0,0 +1,3 @@
defmodule PolicyService.EventStore do
use EventStore, otp_app: :policy_service
end

View File

@@ -0,0 +1,69 @@
defmodule PolicyService.Events.Car.CarPolicyApplicationSubmitted do
@derive Jason.Encoder
defstruct [
:application_id,
:org_id,
:submitted_by,
:applicant_info,
:car_details,
:selected_providers,
:submitted_at
]
end
defmodule PolicyService.Events.Car.CarQuoteRequestSent do
@derive Jason.Encoder
defstruct [
:application_id,
:org_id,
:provider_id,
:provider_email,
:applicant_info,
:car_details,
:requested_at
]
end
defmodule PolicyService.Events.Car.CarProviderQuoteReceived do
@derive Jason.Encoder
defstruct [
:application_id,
:org_id,
:recorded_by,
:provider_id,
:quote_id,
:premium,
:coverage_details,
:valid_until,
:received_at
]
end
defmodule PolicyService.Events.Car.AllCarQuotesReceived do
@derive Jason.Encoder
defstruct [:application_id, :org_id, :quote_count]
end
defmodule PolicyService.Events.Car.CarQuoteAccepted do
@derive Jason.Encoder
defstruct [:application_id, :org_id, :accepted_by, :quote_id, :provider_id, :accepted_at]
end
defmodule PolicyService.Events.Car.CarSolicitationSent do
@derive Jason.Encoder
defstruct [:application_id, :org_id, :provider_id, :quote_id, :sent_at]
end
defmodule PolicyService.Events.Car.CarPolicyIssued do
@derive Jason.Encoder
defstruct [
:application_id,
:org_id,
:recorded_by,
:policy_number,
:provider_id,
:effective_date,
:expiry_date,
:issued_at
]
end

View File

@@ -0,0 +1,21 @@
defmodule PolicyService.Handlers.QuoteRequestHandler do
use Commanded.Event.Handler,
application: PolicyService.CommandedApp,
name: __MODULE__
alias PolicyService.Events.Car.CarQuoteRequestSent
# alias PolicyService.Events.Life.LifeQuoteRequestSent
# alias PolicyService.Events.Fire.FireQuoteRequestSent
def handle(%CarQuoteRequestSent{} = e, _metadata) do
PolicyService.MessageBus.publish("carquote.requested", e)
end
# def handle(%LifeQuoteRequestSent{} = e, _metadata) do
# PolicyService.MessageBus.publish("quote.requested", e)
# end
# def handle(%FireQuoteRequestSent{} = e, _metadata) do
# PolicyService.MessageBus.publish("quote.requested", e)
# end
end

View File

@@ -0,0 +1,22 @@
defmodule PolicyService.MessageBus do
use AMQP
def publish(routing_key, event) do
payload = Jason.encode!(event)
:ok =
AMQP.Basic.publish(channel(), "policy_service.events", routing_key, payload,
content_type: "application/json",
# survives RabbitMQ restart
persistent: true
)
end
defp channel do
{:ok, conn} = AMQP.Connection.open(amqp_url())
{:ok, chan} = AMQP.Channel.open(conn)
chan
end
defp amqp_url, do: Application.fetch_env!(:policy_service, :amqp_url)
end

View File

@@ -0,0 +1,13 @@
defmodule PolicyService.Repo do
use Ecto.Repo,
otp_app: :policy_service,
adapter: Ecto.Adapters.Postgres
@doc """
Dynamically loads the repository url from the
DATABASE_URL environment variable.
"""
def init(_, opts) do
{:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
end
end

63
lib/policy_service_web.ex Normal file
View File

@@ -0,0 +1,63 @@
defmodule PolicyServiceWeb do
@moduledoc """
The entrypoint for defining your web interface, such
as controllers, components, channels, and so on.
This can be used in your application as:
use PolicyServiceWeb, :controller
use PolicyServiceWeb, :html
The definitions below will be executed for every controller,
component, etc, so keep them short and clean, focused
on imports, uses and aliases.
Do NOT define functions inside the quoted expressions
below. Instead, define additional modules and import
those modules here.
"""
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
def router do
quote do
use Phoenix.Router, helpers: false
# Import common connection and controller functions to use in pipelines
import Plug.Conn
import Phoenix.Controller
end
end
def channel do
quote do
use Phoenix.Channel
end
end
def controller do
quote do
use Phoenix.Controller, formats: [:html, :json]
import Plug.Conn
unquote(verified_routes())
end
end
def verified_routes do
quote do
use Phoenix.VerifiedRoutes,
endpoint: PolicyServiceWeb.Endpoint,
router: PolicyServiceWeb.Router,
statics: PolicyServiceWeb.static_paths()
end
end
@doc """
When used, dispatch to the appropriate controller/live_view/etc.
"""
defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end

View File

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

View File

@@ -0,0 +1,85 @@
# lib/policy_service_web/controllers/car_policy_controller.ex
defmodule PolicyServiceWeb.CarPolicyController do
use PolicyServiceWeb, :controller
use OpenApiSpex.ControllerSpecs
alias OpenApiSpex.Schema
alias PolicyServiceWeb.Schemas.CarPolicy.{QuoteRequest, QuoteResponse}
alias PolicyService.Commands.Car.SubmitCarPolicyApplication
tags(["Car Policy"])
security([%{"bearerAuth" => []}])
operation(:request_quote,
summary: "Solicitar cotización de seguro de auto",
description: "Envía una solicitud de cotización a los proveedores seleccionados",
request_body: {"Quote request body", "application/json", QuoteRequest, required: true},
responses: [
created: {"Solicitud creada exitosamente", "application/json", QuoteResponse},
unprocessable_entity:
{"Error de validación", "application/json",
%Schema{
type: :object,
properties: %{
errors: %Schema{type: :object}
}
}}
]
)
def request_quote(conn, params) do
user = %{"id" => "test", "org_id" => "test"}
cmd = %SubmitCarPolicyApplication{
application_id: Ecto.UUID.generate(),
org_id: user["org_id"],
submitted_by: user["id"],
applicant_info: %{
name: params["applicant_info"]["name"],
date_of_birth: Date.from_iso8601!(params["applicant_info"]["date_of_birth"]),
document_id: params["applicant_info"]["document_id"]
},
car_details: %{
plate: params["car_details"]["plate"],
make: params["car_details"]["make"],
model: params["car_details"]["model"],
year: params["car_details"]["year"],
car_value: parse_number(params["car_details"]["car_value"]),
use_type: String.to_atom(params["car_details"]["use_type"]),
car_type: String.to_atom(params["car_details"]["car_type"]),
chassis_number: params["car_details"]["chassis_number"],
engine_number: params["car_details"]["engine_number"]
},
selected_providers:
Enum.map(params["selected_providers"], fn p ->
%{id: p["id"], email: p["email"]}
end)
}
case PolicyService.CommandedApp.dispatch(cmd) do
:ok ->
conn
|> put_status(:created)
|> json(%{
application_id: cmd.applicant_info,
status: "awaiting_quotes"
})
{:error, reason} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{errors: reason})
end
end
defp parse_number(val) when is_float(val), do: val
defp parse_number(val) when is_integer(val), do: val * 1.0
defp parse_number(val) when is_binary(val) do
case Float.parse(val) do
{f, _} -> f
:error -> raise "invalid number: #{val}"
end
end
end

View File

@@ -0,0 +1,21 @@
defmodule PolicyServiceWeb.ErrorJSON do
@moduledoc """
This module is invoked by your endpoint in case of errors on JSON requests.
See config/config.exs.
"""
# If you want to customize a particular status code,
# you may add your own clauses, such as:
#
# def render("500.json", _assigns) do
# %{errors: %{detail: "Internal Server Error"}}
# end
# By default, Phoenix returns the status message from
# the template name. For example, "404.json" becomes
# "Not Found".
def render(template, _assigns) do
%{errors: %{detail: Phoenix.Controller.status_message_from_template(template)}}
end
end

View File

@@ -0,0 +1,49 @@
defmodule PolicyServiceWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :policy_service
# The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it.
@session_options [
store: :cookie,
key: "_policy_service_key",
signing_salt: "9eYllgTe",
same_site: "Lax"
]
# socket "/live", Phoenix.LiveView.Socket,
# websocket: [connect_info: [session: @session_options]],
# longpoll: [connect_info: [session: @session_options]]
# Serve at "/" the static files from "priv/static" directory.
#
# When code reloading is disabled (e.g., in production),
# the `gzip` option is enabled to serve compressed
# static files generated by running `phx.digest`.
plug Plug.Static,
at: "/",
from: :policy_service,
gzip: not code_reloading?,
only: PolicyServiceWeb.static_paths(),
raise_on_missing_only: code_reloading?
# Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint.
if code_reloading? do
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :policy_service
end
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug PolicyServiceWeb.Router
end

View File

@@ -0,0 +1,26 @@
defmodule PolicyServiceWeb.Router do
use PolicyServiceWeb, :router
pipeline :api do
plug OpenApiSpex.Plug.PutApiSpec, module: PolicyServiceWeb.ApiSpec
end
scope "/api" do
pipe_through [:api]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
scope "/v1" do
scope "/car-policies" do
post "/quotes", PolicyServiceWeb.CarPolicyController, :request_quote
end
end
end
# Swagger UI — only in dev
if Mix.env() == :dev do
scope "/swaggerui" do
get "/", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
end
end
end

View File

@@ -0,0 +1,114 @@
defmodule PolicyServiceWeb.Schemas.CarPolicy do
alias OpenApiSpex.Schema
defmodule ApplicantInfo do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "ApplicantInfo",
type: :object,
required: [:name, :date_of_birth, :document_id],
properties: %{
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: "V-12345678"}
}
})
end
defmodule CarDetails do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "CarDetails",
type: :object,
required: [
:plate,
:make,
:model,
:year,
:car_value,
:use_type,
:car_type,
:chassis_number,
:engine_number
],
properties: %{
plate: %Schema{type: :string, example: "ABC-1234"},
make: %Schema{type: :string, example: "Toyota"},
model: %Schema{type: :string, example: "Corolla"},
year: %Schema{type: :integer, example: 2022},
car_value: %Schema{type: :number, example: 18000},
use_type: %Schema{
type: :string,
enum: ["private", "commercial", "bus", "taxi", "school"],
example: "private"
},
car_type: %Schema{
type: :string,
enum: [
"sedan",
"suv",
"hatchback",
"coupe",
"convertible",
"pickup",
"van",
"minivan",
"truck"
],
example: "sedan"
},
chassis_number: %Schema{type: :string, example: "9BWZZZ377VT004251"},
engine_number: %Schema{type: :string, example: "1NZ-FE-1234567"}
}
})
end
defmodule Provider do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "Provider",
type: :object,
required: [:id, :email],
properties: %{
id: %Schema{type: :string, example: "provider-uuid"},
email: %Schema{type: :string, format: :email, example: "cotizaciones@aseguradora.com"}
}
})
end
defmodule QuoteRequest do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "QuoteRequest",
type: :object,
required: [:applicant_info, :car_details, :selected_providers],
properties: %{
applicant_info: ApplicantInfo,
car_details: CarDetails,
selected_providers: %Schema{
type: :array,
items: Provider,
minItems: 1,
example: [%{id: "provider-uuid", email: "cotizaciones@aseguradora.com"}]
}
}
})
end
defmodule QuoteResponse do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "QuoteResponse",
type: :object,
properties: %{
application_id: %Schema{type: :string, example: "550e8400-e29b-41d4-a716-446655440000"},
status: %Schema{type: :string, example: "awaiting_quotes"}
}
})
end
end

View File

@@ -0,0 +1,93 @@
defmodule PolicyServiceWeb.Telemetry do
use Supervisor
import Telemetry.Metrics
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
children = [
# Telemetry poller will execute the given period measurements
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
# Add reporters as children of your supervision tree.
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
]
Supervisor.init(children, strategy: :one_for_one)
end
def metrics do
[
# Phoenix Metrics
summary("phoenix.endpoint.start.system_time",
unit: {:native, :millisecond}
),
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.start.system_time",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.exception.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
summary("phoenix.socket_connected.duration",
unit: {:native, :millisecond}
),
sum("phoenix.socket_drain.count"),
summary("phoenix.channel_joined.duration",
unit: {:native, :millisecond}
),
summary("phoenix.channel_handled_in.duration",
tags: [:event],
unit: {:native, :millisecond}
),
# Database Metrics
summary("policy_service.repo.query.total_time",
unit: {:native, :millisecond},
description: "The sum of the other measurements"
),
summary("policy_service.repo.query.decode_time",
unit: {:native, :millisecond},
description: "The time spent decoding the data received from the database"
),
summary("policy_service.repo.query.query_time",
unit: {:native, :millisecond},
description: "The time spent executing the query"
),
summary("policy_service.repo.query.queue_time",
unit: {:native, :millisecond},
description: "The time spent waiting for a database connection"
),
summary("policy_service.repo.query.idle_time",
unit: {:native, :millisecond},
description:
"The time the connection spent waiting before being checked out for the query"
),
# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io")
]
end
defp periodic_measurements do
[
# A module, function and arguments to be invoked periodically.
# This function must call :telemetry.execute/3 and a metric must be added above.
# {PolicyServiceWeb, :count_users, []}
]
end
end

75
mix.exs Normal file
View File

@@ -0,0 +1,75 @@
defmodule PolicyService.MixProject do
use Mix.Project
def project do
[
app: :policy_service,
version: "0.1.0",
elixir: "~> 1.15",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
listeners: [Phoenix.CodeReloader]
]
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {PolicyService.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end
def cli do
[
preferred_envs: [precommit: :test]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.8.3"},
{:phoenix_ecto, "~> 4.5"},
{:ecto_sql, "~> 3.13"},
{:postgrex, ">= 0.0.0"},
{:telemetry_metrics, "~> 1.0"},
{:telemetry_poller, "~> 1.0"},
{:jason, "~> 1.2"},
{:dns_cluster, "~> 0.2.0"},
{:bandit, "~> 1.5"},
{:commanded_ecto_projections, "~> 1.4"},
{:commanded_eventstore_adapter, "~> 1.4"},
{:commanded, "~> 1.4"},
{:amqp, "~> 4.1"},
{:exconstructor, "~> 1.3.1"},
{:open_api_spex, "~> 3.20"}
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to install project dependencies and perform other setup tasks, run:
#
# $ mix setup
#
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
setup: ["deps.get", "ecto.setup"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
precommit: ["compile --warnings-as-errors", "deps.unlock --unused", "format", "test"]
]
end
end

46
mix.lock Normal file
View File

@@ -0,0 +1,46 @@
%{
"amqp": {:hex, :amqp, "4.1.0", "ab993d7a7adb41bc52fc084224441bad34ba04a6ff97fcd4a17f8281ed010ed1", [:mix], [{:amqp_client, "~> 4.0", [hex: :amqp_client, repo: "hexpm", optional: false]}], "hexpm", "360559cb5a4c4a920806a8c281d0418db3a625ce730008c8460749dd9b1cf838"},
"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"},
"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_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"},
"crc32cer": {:hex, :crc32cer, "0.1.8", "c6c2275c5fb60a95f4935d414f30b50ee9cfed494081c9b36ebb02edfc2f48db", [:rebar3], [], "hexpm", "251499085482920deb6c9b7aadabf9fb4c432f96add97ab42aee4501e5b6f591"},
"credentials_obfuscation": {:hex, :credentials_obfuscation, "3.5.0", "61e282adfb4439486b3994faaec69543c7ee6cc7e70c6340e8853fd9deaf8219", [:rebar3], [], "hexpm", "843adbe3246861ce0f1a0fa3222f384834eb31defd8d6b9cba7afd2977c957bc"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
"ecto_sql": {:hex, :ecto_sql, "3.13.4", "b6e9d07557ddba62508a9ce4a484989a5bb5e9a048ae0e695f6d93f095c25d60", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b38cf0749ca4d1c5a8bcbff79bbe15446861ca12a61f9fba604486cb6b62a14"},
"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"},
"fsm": {:hex, :fsm, "0.3.1", "087aa9b02779a84320dc7a2d8464452b5308e29877921b2bde81cdba32a12390", [:mix], [], "hexpm", "fbf0d53f89e9082b326b0b5828b94b4c549ff9d1452bbfd00b4d1ac082208e96"},
"gen_stage": {:hex, :gen_stage, "1.3.2", "7c77e5d1e97de2c6c2f78f306f463bca64bf2f4c3cdd606affc0100b89743b7b", [:mix], [], "hexpm", "0ffae547fa777b3ed889a6b9e1e64566217413d018cabd825f786e843ffe63e7"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"kafka_protocol": {:hex, :kafka_protocol, "4.1.5", "d15e64994a8ca99716ab47db4132614359ac1bfa56d6c5b4341fdc1aa4041518", [:rebar3], [{:crc32cer, "0.1.8", [hex: :crc32cer, repo: "hexpm", optional: false]}], "hexpm", "c956c9357fef493b7072a35d0c3e2be02aa5186c804a412d29e62423bb15e5d9"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"open_api_spex": {:hex, :open_api_spex, "3.22.2", "0b3c4f572ee69cb6c936abf426b9d84d8eebd34960871fd77aead746f0d69cb0", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "0a4fc08472d75e9cfe96e0748c6b1565b3b4398f97bf43fcce41b41b6fd3fb33"},
"phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
"rabbit_common": {:hex, :rabbit_common, "4.2.1", "1d64e391e12116b76b1425eb96b7552de51f0301093eba669b5334f4759cc1e8", [:make, :rebar3], [{:credentials_obfuscation, "3.5.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:ranch, "2.2.0", [hex: :ranch, repo: "hexpm", optional: false]}, {:recon, "2.5.6", [hex: :recon, repo: "hexpm", optional: false]}, {:thoas, "1.2.1", [hex: :thoas, repo: "hexpm", optional: false]}], "hexpm", "ff509b07e639b1784898c28031e5204fea14260172e4fc339f94405586037e40"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
"snappyer": {:hex, :snappyer, "1.2.9", "9cc58470798648ce34c662ca0aa6daae31367667714c9a543384430a3586e5d3", [:rebar3], [], "hexpm", "18d00ca218ae613416e6eecafe1078db86342a66f86277bd45c95f05bf1c8b29"},
"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_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"},
"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"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
}

View File

@@ -0,0 +1,4 @@
[
import_deps: [:ecto_sql],
inputs: ["*.exs"]
]

11
priv/repo/seeds.exs Normal file
View File

@@ -0,0 +1,11 @@
# Script for populating the database. You can run it as:
#
# mix run priv/repo/seeds.exs
#
# Inside the script, you can read and write to any of your
# repositories directly:
#
# PolicyService.Repo.insert!(%PolicyService.SomeSchema{})
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.

BIN
priv/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

5
priv/static/robots.txt Normal file
View File

@@ -0,0 +1,5 @@
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
# Disallow: /

91
rabbitmq/definitions.json Normal file
View File

@@ -0,0 +1,91 @@
{
"vhosts": [
{
"name": "/"
}
],
"permissions": [
{
"user": "guest",
"vhost": "/",
"configure": ".*",
"write": ".*",
"read": ".*"
}
],
"exchanges": [
{
"name": "policy_service.events",
"vhost": "/",
"type": "topic",
"durable": true,
"auto_delete": false,
"arguments": {}
},
{
"name": "carrier_inbox.events",
"vhost": "/",
"type": "topic",
"durable": true,
"auto_delete": false,
"arguments": {}
}
],
"queues": [
{
"name": "carquote.requested",
"vhost": "/",
"durable": true,
"auto_delete": false,
"arguments": {}
},
{
"name": "carsolicitation.sent",
"vhost": "/",
"durable": true,
"auto_delete": false,
"arguments": {}
},
{
"name": "policy_service.quote_received",
"vhost": "/",
"durable": true,
"auto_delete": false,
"arguments": {}
}
],
"bindings": [
{
"source": "policy_service.events",
"vhost": "/",
"destination": "carquote.requested",
"destination_type": "queue",
"routing_key": "carquote.requested",
"arguments": {}
},
{
"source": "policy_service.events",
"vhost": "/",
"destination": "carsolicitation.sent",
"destination_type": "queue",
"routing_key": "carsolicitation.sent",
"arguments": {}
},
{
"source": "carrier_inbox.events",
"vhost": "/",
"destination": "policy_service.quote_received",
"destination_type": "queue",
"routing_key": "quote.received",
"arguments": {}
}
],
"users": [
{
"name": "guest",
"password_hash": "rE1pU3Tq9IG79e0m1W4Yp2gSHFvr4DDF5m4H0U6+0Zgu5BFb",
"hashing_algorithm": "rabbit_password_hashing_sha256",
"tags": "administrator"
}
]
}

2
rabbitmq/rabbitmq.conf Normal file
View File

@@ -0,0 +1,2 @@
loopback_users.guest = false
load_definitions = /etc/rabbitmq/definitions.json

View File

@@ -0,0 +1,8 @@
defmodule PolicyServiceTest do
use ExUnit.Case
doctest PolicyService
test "greets the world" do
assert PolicyService.hello() == :world
end
end

View File

@@ -0,0 +1,12 @@
defmodule PolicyServiceWeb.ErrorJSONTest do
use PolicyServiceWeb.ConnCase, async: true
test "renders 404" do
assert PolicyServiceWeb.ErrorJSON.render("404.json", %{}) == %{errors: %{detail: "Not Found"}}
end
test "renders 500" do
assert PolicyServiceWeb.ErrorJSON.render("500.json", %{}) ==
%{errors: %{detail: "Internal Server Error"}}
end
end

38
test/support/conn_case.ex Normal file
View File

@@ -0,0 +1,38 @@
defmodule PolicyServiceWeb.ConnCase do
@moduledoc """
This module defines the test case to be used by
tests that require setting up a connection.
Such tests rely on `Phoenix.ConnTest` and also
import other functionality to make it easier
to build common data structures and query the data layer.
Finally, if the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use PolicyServiceWeb.ConnCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
# The default endpoint for testing
@endpoint PolicyServiceWeb.Endpoint
use PolicyServiceWeb, :verified_routes
# Import conveniences for testing with connections
import Plug.Conn
import Phoenix.ConnTest
import PolicyServiceWeb.ConnCase
end
end
setup tags do
PolicyService.DataCase.setup_sandbox(tags)
{:ok, conn: Phoenix.ConnTest.build_conn()}
end
end

58
test/support/data_case.ex Normal file
View File

@@ -0,0 +1,58 @@
defmodule PolicyService.DataCase do
@moduledoc """
This module defines the setup for tests requiring
access to the application's data layer.
You may define functions here to be used as helpers in
your tests.
Finally, if the test case interacts with the database,
we enable the SQL sandbox, so changes done to the database
are reverted at the end of every test. If you are using
PostgreSQL, you can even run database tests asynchronously
by setting `use PolicyService.DataCase, async: true`, although
this option is not recommended for other databases.
"""
use ExUnit.CaseTemplate
using do
quote do
alias PolicyService.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import PolicyService.DataCase
end
end
setup tags do
PolicyService.DataCase.setup_sandbox(tags)
:ok
end
@doc """
Sets up the sandbox based on the test tags.
"""
def setup_sandbox(tags) do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(PolicyService.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
end
@doc """
A helper that transforms changeset errors into a map of messages.
assert {:error, changeset} = Accounts.create_user(%{password: "short"})
assert "password is too short" in errors_on(changeset).password
assert %{password: ["password is too short"]} = errors_on(changeset)
"""
def errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
end)
end)
end
end

2
test/test_helper.exs Normal file
View File

@@ -0,0 +1,2 @@
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(PolicyService.Repo, :manual)