diff --git a/.credo.exs b/.credo.exs
index 4eddee8..d871572 100644
--- a/.credo.exs
+++ b/.credo.exs
@@ -158,11 +158,11 @@
{Credo.Check.Warning.UnusedRegexOperation, []},
{Credo.Check.Warning.UnusedStringOperation, []},
{Credo.Check.Warning.UnusedTupleOperation, []},
- {Credo.Check.Warning.WrongTestFileExtension, []},
- # Module documentation check (enabled after adding @moduledoc to all modules)
- {Credo.Check.Readability.ModuleDoc, []}
+ {Credo.Check.Warning.WrongTestFileExtension, []}
],
disabled: [
+ # Checks disabled by the Mitgliederverwaltung Team
+ {Credo.Check.Readability.ModuleDoc, []},
#
# Checks scheduled for next check update (opt-in for now)
{Credo.Check.Refactor.UtcNowTruncate, []},
diff --git a/CODE_GUIDELINES.md b/CODE_GUIDELINES.md
deleted file mode 100644
index 4a82edb..0000000
--- a/CODE_GUIDELINES.md
+++ /dev/null
@@ -1,2578 +0,0 @@
-# Code Guidelines and Best Practices
-
-## Purpose
-
-This document serves as a foundational reference for our development team, ensuring consistency, maintainability, and quality in our codebase. It defines standards and best practices for building the Mila membership management application.
-
-## Project Context
-
-We are building a membership management system (Mila) using the following technology stack:
-
-**Backend & Runtime:**
-- Elixir `~> 1.15` (currently 1.18.3-otp-27)
-- Erlang/OTP 27.3.4
-- Phoenix Framework `~> 1.8.0`
-- Ash Framework `~> 3.0`
-- AshPostgres `~> 2.0`
-- Ecto `~> 3.10`
-- Postgrex `>= 0.0.0`
-- AshAuthentication `~> 4.9`
-- AshAuthenticationPhoenix `~> 2.10`
-- bcrypt_elixir `~> 3.0`
-
-**Frontend & UI:**
-- Phoenix LiveView `~> 1.1.0`
-- Phoenix HTML `~> 4.1`
-- Tailwind CSS 4.0.9
-- DaisyUI (as Tailwind plugin)
-- Heroicons v2.2.0
-- JavaScript (ES2022)
-- esbuild `~> 0.9`
-
-**Database:**
-- PostgreSQL 17.6 (dev), 16 (prod)
-
-**Testing:**
-- ExUnit (built-in)
-- Ecto.Adapters.SQL.Sandbox
-
-**Development Tools:**
-- asdf 0.16.5 (version management)
-- Just 1.43.0 (task runner)
-- Credo `~> 1.7` (code analysis)
-- Sobelow `~> 0.14` (security analysis)
-- mix_audit `~> 2.1` (dependency audit)
-
-**Infrastructure:**
-- Docker & Docker Compose
-- Bandit `~> 1.5` (HTTP server)
-
----
-
-## Table of Contents
-
-1. [Setup and Architectural Conventions](#1-setup-and-architectural-conventions)
-2. [Coding Standards and Style](#2-coding-standards-and-style)
-3. [Tooling Guidelines](#3-tooling-guidelines)
-4. [Testing Standards](#4-testing-standards)
-5. [Security Guidelines](#5-security-guidelines)
-6. [Performance Best Practices](#6-performance-best-practices)
-7. [Documentation Standards](#7-documentation-standards)
-8. [Accessibility Guidelines](#8-accessibility-guidelines)
-
----
-
-## 1. Setup and Architectural Conventions
-
-### 1.1 Project Structure
-
-Our project follows a domain-driven design approach using Phoenix contexts and Ash domains:
-
-```
-lib/
-├── accounts/ # Accounts domain (AshAuthentication)
-│ ├── accounts.ex # Domain definition
-│ ├── user.ex # User resource
-│ ├── token.ex # Token resource
-│ ├── user_identity.exs # User identity helpers
-│ └── user/ # User-related modules
-│ ├── changes/ # Ash changes for user
-│ └── preparations/ # Ash preparations for user
-├── membership/ # Membership domain
-│ ├── membership.ex # Domain definition
-│ ├── member.ex # Member resource
-│ ├── property.ex # Custom property resource
-│ ├── property_type.ex # Property type resource
-│ └── email.ex # Email custom type
-├── mv/ # Core application modules
-│ ├── accounts/ # Domain-specific logic
-│ │ └── user/
-│ │ ├── senders/ # Email senders for user actions
-│ │ └── validations/
-│ ├── email_sync/ # Email synchronization logic
-│ │ ├── changes/ # Sync changes
-│ │ ├── helpers.ex # Sync helper functions
-│ │ └── loader.ex # Data loaders
-│ ├── membership/ # Domain-specific logic
-│ │ └── member/
-│ │ └── validations/
-│ ├── application.ex # OTP application
-│ ├── mailer.ex # Email mailer
-│ ├── release.ex # Release tasks
-│ ├── repo.ex # Database repository
-│ └── secrets.ex # Secret management
-├── mv_web/ # Web interface layer
-│ ├── components/ # UI components
-│ │ ├── core_components.ex
-│ │ ├── table_components.ex
-│ │ ├── layouts.ex
-│ │ └── layouts/ # Layout templates
-│ │ ├── navbar.ex
-│ │ └── root.html.heex
-│ ├── controllers/ # HTTP controllers
-│ │ ├── auth_controller.ex
-│ │ ├── page_controller.ex
-│ │ ├── locale_controller.ex
-│ │ ├── error_html.ex
-│ │ ├── error_json.ex
-│ │ └── page_html/
-│ ├── live/ # LiveView modules
-│ │ ├── components/ # LiveView-specific components
-│ │ │ ├── search_bar_component.ex
-│ │ │ └── sort_header_component.ex
-│ │ ├── member_live/ # Member CRUD LiveViews
-│ │ ├── property_live/ # Property CRUD LiveViews
-│ │ ├── property_type_live/
-│ │ └── user_live/ # User management LiveViews
-│ ├── auth_overrides.ex # AshAuthentication overrides
-│ ├── endpoint.ex # Phoenix endpoint
-│ ├── gettext.ex # I18n configuration
-│ ├── live_helpers.ex # LiveView helpers
-│ ├── live_user_auth.ex # LiveView authentication
-│ ├── router.ex # Application router
-│ └── telemetry.ex # Telemetry configuration
-├── mv_web.ex # Web module definition
-└── mv.ex # Application module definition
-
-test/
-├── accounts/ # Accounts domain tests
-│ ├── user_test.exs
-│ ├── email_sync_edge_cases_test.exs
-│ ├── email_uniqueness_test.exs
-│ ├── user_email_sync_test.exs
-│ ├── user_member_deletion_test.exs
-│ └── user_member_relationship_test.exs
-├── membership/ # Membership domain tests
-│ ├── member_test.exs
-│ └── member_email_sync_test.exs
-├── mv_web/ # Web layer tests
-│ ├── components/ # Component tests
-│ │ ├── layouts/
-│ │ │ └── navbar_test.exs
-│ │ ├── search_bar_component_test.exs
-│ │ └── sort_header_component_test.exs
-│ ├── controllers/ # Controller tests
-│ │ ├── auth_controller_test.exs
-│ │ ├── error_html_test.exs
-│ │ ├── error_json_test.exs
-│ │ ├── oidc_integration_test.exs
-│ │ └── page_controller_test.exs
-│ ├── live/ # LiveView tests
-│ │ └── profile_navigation_test.exs
-│ ├── member_live/ # Member LiveView tests
-│ │ └── index_test.exs
-│ ├── user_live/ # User LiveView tests
-│ │ ├── form_test.exs
-│ │ └── index_test.exs
-│ └── locale_test.exs
-├── seeds_test.exs # Database seed tests
-└── support/ # Test helpers
- ├── conn_case.ex # Controller test helpers
- └── data_case.ex # Data layer test helpers
-```
-
-### 1.2 Module Organization
-
-**Module Naming:**
-
-- **Modules:** Use `PascalCase` with full namespace (e.g., `Mv.Accounts.User`)
-- **Domains:** Top-level domains are `Mv.Accounts` and `Mv.Membership`
-- **Resources:** Resource modules should be singular nouns (e.g., `Member`, not `Members`)
-- **Context functions:** Use `snake_case` and verb-first naming (e.g., `create_user`, `list_members`)
-
-**Module Structure:**
-
-```elixir
-defmodule Mv.Membership.Member do
- @moduledoc """
- Represents a club member with their personal information and membership status.
- """
-
- use Ash.Resource,
- domain: Mv.Membership,
- data_layer: AshPostgres.DataLayer
-
- # 1. Ash DSL sections in order (see Spark formatter config)
- admin do
- # ...
- end
-
- postgres do
- # ...
- end
-
- resource do
- # ...
- end
-
- code_interface do
- # ...
- end
-
- actions do
- # ...
- end
-
- policies do
- # ...
- end
-
- attributes do
- # ...
- end
-
- relationships do
- # ...
- end
-
- # 2. Public functions
-
- # 3. Private functions
-end
-```
-
-### 1.3 Domain-Driven Design
-
-**Use Ash Domains for Context Boundaries:**
-
-Each domain should:
-- Have a clear boundary and responsibility
-- Define a public API through code interfaces
-- Encapsulate business logic within resources
-- Handle cross-domain communication explicitly
-
-Example domain definition:
-
-```elixir
-defmodule Mv.Membership do
- use Ash.Domain,
- extensions: [AshAdmin.Domain, AshPhoenix]
-
- admin do
- show? true
- end
-
- resources do
- resource Mv.Membership.Member do
- define :create_member, action: :create_member
- define :list_members, action: :read
- define :update_member, action: :update_member
- define :destroy_member, action: :destroy
- end
- end
-end
-```
-
-### 1.4 Dependency Management
-
-- **Use `mix.exs` for all dependencies:** Define versions explicitly
-- **Keep dependencies up to date:** Use Renovate for automated updates
-- **Version management:** Use `asdf` with `.tool-versions` for consistent environments
-
-### 1.5 Scalability Considerations
-
-- **Database indexing:** Add indexes for frequently queried fields
-- **Pagination:** Use Ash's keyset pagination for large datasets (default configured)
-- **Background jobs:** Plan for Oban or similar for async processing
-- **Caching:** Consider caching strategies for expensive operations
-- **Process design:** Use OTP principles (GenServers, Supervisors) for stateful components
-
----
-
-## 2. Coding Standards and Style
-
-### 2.1 Code Formatting
-
-**Use `mix format` for all Elixir code:**
-
-```bash
-mix format
-```
-
-**Key formatting rules:**
-- **Indentation:** 2 spaces (no tabs)
-- **Line length:** Maximum 120 characters (configured in `.credo.exs`)
-- **Trailing whitespace:** Not allowed
-- **File endings:** Always include trailing newline
-
-**Naming Conventions Summary:**
-
-- **Elixir:** Use `snake_case` for functions/variables, `PascalCase` for modules
-- **Phoenix:** Controllers end with `Controller`, LiveViews end with `Live`
-- **Ash:** Resources are singular nouns, actions are verb-first (`:create_member`)
-- **Files:** Match module names in `snake_case` (`user_controller.ex` for `UserController`)
-
-### 2.2 Function Design
-
-**Verb-First Function Names:**
-
-```elixir
-# Good
-def create_user(attrs)
-def list_members(query)
-def send_email(recipient, content)
-
-# Avoid
-def user_create(attrs)
-def members_list(query)
-def email_send(recipient, content)
-```
-
-**Use Pattern Matching in Function Heads:**
-
-```elixir
-# Good - multiple clauses with pattern matching
-def handle_result({:ok, user}), do: {:ok, user}
-def handle_result({:error, reason}), do: log_and_return_error(reason)
-
-# Avoid - case/cond when pattern matching suffices
-def handle_result(result) do
- case result do
- {:ok, user} -> {:ok, user}
- {:error, reason} -> log_and_return_error(reason)
- end
-end
-```
-
-**Keep Functions Small and Focused:**
-
-- Aim for functions under 20 lines
-- Each function should have a single responsibility
-- Extract complex logic into private helper functions
-
-**Use Guard Clauses for Early Returns:**
-
-```elixir
-def process_user(nil), do: {:error, :user_not_found}
-def process_user(%{active: false}), do: {:error, :user_inactive}
-def process_user(user), do: {:ok, perform_action(user)}
-```
-
-### 2.3 Error Handling
-
-**Use Tagged Tuples:**
-
-```elixir
-# Standard pattern
-{:ok, result} | {:error, reason}
-
-# Examples
-def create_member(attrs) do
- case Ash.create(Member, attrs) do
- {:ok, member} -> {:ok, member}
- {:error, error} -> {:error, error}
- end
-end
-```
-
-**Use `with` for Complex Operations:**
-
-```elixir
-def register_user(params) do
- with {:ok, validated} <- validate_params(params),
- {:ok, user} <- create_user(validated),
- {:ok, _email} <- send_welcome_email(user) do
- {:ok, user}
- else
- {:error, reason} -> {:error, reason}
- end
-end
-```
-
-**Let It Crash (with Supervision):**
-
-Don't defensively program against every possible error. Use supervisors to handle process failures:
-
-```elixir
-# In your application.ex
-children = [
- Mv.Repo,
- MvWeb.Endpoint,
- {Phoenix.PubSub, name: Mv.PubSub}
-]
-
-Supervisor.start_link(children, strategy: :one_for_one)
-```
-
-### 2.4 Functional Programming Principles
-
-**Immutability:**
-
-```elixir
-# Good - return new data structures
-def add_role(user, role) do
- %{user | roles: [role | user.roles]}
-end
-
-# Avoid - mutation (not possible in Elixir anyway)
-# This is just conceptual - Elixir prevents mutation
-```
-
-**Pure Functions:**
-
-Write functions that:
-- Return the same output for the same input
-- Have no side effects
-- Are easier to test and reason about
-
-```elixir
-# Pure function
-def calculate_total(items) do
- Enum.reduce(items, 0, fn item, acc -> acc + item.price end)
-end
-
-# Impure function (side effects)
-def create_and_log_user(attrs) do
- Logger.info("Creating user: #{inspect(attrs)}") # Side effect
- Ash.create!(User, attrs) # Side effect
-end
-```
-
-**Pipe Operator:**
-
-Use the pipe operator `|>` for transformation chains:
-
-```elixir
-# Good
-def process_members(query) do
- query
- |> filter_active()
- |> sort_by_name()
- |> limit_results(10)
-end
-
-# Avoid
-def process_members(query) do
- limit_results(sort_by_name(filter_active(query)), 10)
-end
-```
-
-### 2.5 Elixir-Specific Patterns
-
-**Avoid Using Else with Unless:**
-
-```elixir
-# Good
-unless user.admin? do
- {:error, :unauthorized}
-end
-
-# Avoid - confusing
-unless user.admin? do
- {:error, :unauthorized}
-else
- perform_admin_action()
-end
-```
-
-**Use `Enum` over List Comprehensions for Clarity:**
-
-```elixir
-# Preferred for readability
-users
-|> Enum.filter(&(&1.active))
-|> Enum.map(&(&1.name))
-
-# List comprehension (use when more concise)
-for user <- users, user.active, do: user.name
-```
-
-**String Concatenation:**
-
-```elixir
-# Good - interpolation
-"Hello, #{user.name}!"
-
-# Avoid - concatenation with <>
-"Hello, " <> user.name <> "!"
-```
-
----
-
-## 3. Tooling Guidelines
-
-### 3.1 Elixir & Erlang/OTP
-
-**Version Management with asdf:**
-
-Always use the versions specified in `.tool-versions`:
-
-```bash
-# Install correct versions
-asdf install
-
-# Verify versions
-elixir --version # Should show 1.18.3
-erl -version # Should show 27.3.4
-```
-
-**OTP Application Design:**
-
-```elixir
-defmodule Mv.Application do
- use Application
-
- def start(_type, _args) do
- children = [
- # Start the database repository
- Mv.Repo,
- # Start the Telemetry supervisor
- MvWeb.Telemetry,
- # Start the PubSub system
- {Phoenix.PubSub, name: Mv.PubSub},
- # Start the Endpoint
- MvWeb.Endpoint
- ]
-
- opts = [strategy: :one_for_one, name: Mv.Supervisor]
- Supervisor.start_link(children, opts)
- end
-end
-```
-
-### 3.2 Phoenix Framework
-
-**Context-Based Organization:**
-
-- Use contexts to define API boundaries
-- Keep controllers thin - delegate to contexts or Ash actions
-- Avoid direct Repo/Ecto calls in controllers
-
-```elixir
-# Good - thin controller
-defmodule MvWeb.MemberController do
- use MvWeb, :controller
-
- def create(conn, %{"member" => member_params}) do
- case Mv.Membership.create_member(member_params) do
- {:ok, member} ->
- conn
- |> put_flash(:info, "Member created successfully.")
- |> redirect(to: ~p"/members/#{member}")
-
- {:error, error} ->
- conn
- |> put_flash(:error, "Failed to create member.")
- |> render(:new, error: error)
- end
- end
-end
-```
-
-**Phoenix LiveView Best Practices:**
-
-```elixir
-defmodule MvWeb.MemberLive.Index do
- use MvWeb, :live_view
-
- # Use mount for initial setup
- def mount(_params, _session, socket) do
- {:ok, assign(socket, members: [], loading: true)}
- end
-
- # Use handle_params for URL parameter handling
- def handle_params(params, _url, socket) do
- {:noreply, apply_action(socket, socket.assigns.live_action, params)}
- end
-
- # Use handle_event for user interactions
- def handle_event("delete", %{"id" => id}, socket) do
- # Handle deletion
- {:noreply, socket}
- end
-
- # Use handle_info for asynchronous messages
- def handle_info({:member_updated, member}, socket) do
- {:noreply, update_member_in_list(socket, member)}
- end
-end
-```
-
-**Component Design:**
-
-```elixir
-# Function components for stateless UI elements
-def button(assigns) do
- ~H"""
-
- """
-end
-
-# Use attrs and slots for documentation
-attr :id, :string, required: true
-attr :title, :string, default: nil
-slot :inner_block, required: true
-
-def card(assigns) do
- ~H"""
-
-
<%= @title %>
- <%= render_slot(@inner_block) %>
-
- """
-end
-```
-
-### 3.3 Ash Framework
-
-**Resource Definition Best Practices:**
-
-```elixir
-defmodule Mv.Membership.Member do
- use Ash.Resource,
- domain: Mv.Membership,
- data_layer: AshPostgres.DataLayer
-
- # Follow section order from Spark formatter config
- postgres do
- table "members"
- repo Mv.Repo
- end
-
- attributes do
- uuid_primary_key :id
-
- attribute :first_name, :string do
- allow_nil? false
- public? true
- end
-
- attribute :email, :string do
- allow_nil? false
- public? true
- end
-
- timestamps()
- end
-
- actions do
- # Define specific actions instead of using defaults.accept_all
- create :create_member do
- accept [:first_name, :last_name, :email]
-
- change fn changeset, _context ->
- # Custom validation or transformation
- changeset
- end
- end
-
- read :read do
- primary? true
- end
-
- update :update_member do
- accept [:first_name, :last_name, :email]
- end
-
- destroy :destroy
- end
-
- code_interface do
- define :create_member
- define :list_members, action: :read
- define :update_member
- define :destroy_member, action: :destroy
- end
-
- identities do
- identity :unique_email, [:email]
- end
-end
-```
-
-**Ash Policies:**
-
-```elixir
-policies do
- # Admin can do everything
- policy action_type([:read, :create, :update, :destroy]) do
- authorize_if actor_attribute_equals(:role, :admin)
- end
-
- # Users can only read and update their own data
- policy action_type([:read, :update]) do
- authorize_if relates_to_actor_via(:user)
- end
-end
-```
-
-**Ash Validations:**
-
-```elixir
-validations do
- validate present(:email), on: [:create, :update]
- validate match(:email, ~r/@/), message: "must be a valid email"
- validate string_length(:first_name, min: 2, max: 100)
-end
-```
-
-### 3.4 AshPostgres & Ecto
-
-**Migrations with Ash:**
-
-```bash
-# Generate migration for all changes
-mix ash.codegen --name add_members_table
-
-# Apply migrations
-mix ash.setup
-```
-
-**Repository Configuration:**
-
-```elixir
-defmodule Mv.Repo do
- use AshPostgres.Repo,
- otp_app: :mv
-
- # Install PostgreSQL extensions
- def installed_extensions do
- ["citext", "uuid-ossp"]
- end
-end
-```
-
-**Avoid N+1 Queries:**
-
-```elixir
-# Good - preload relationships
-members =
- Member
- |> Ash.Query.load(:properties)
- |> Mv.Membership.list_members!()
-
-# Avoid - causes N+1 queries
-members = Mv.Membership.list_members!()
-Enum.map(members, fn member ->
- # This triggers a query for each member
- Ash.load!(member, :properties)
-end)
-```
-
-### 3.5 Authentication (AshAuthentication)
-
-**Resource with Authentication:**
-
-```elixir
-defmodule Mv.Accounts.User do
- use Ash.Resource,
- domain: Mv.Accounts,
- data_layer: AshPostgres.DataLayer,
- extensions: [AshAuthentication]
-
- authentication do
- strategies do
- password :password do
- identity_field :email
- hashed_password_field :hashed_password
- end
-
- oauth2 :rauthy do
- client_id fn _, _ ->
- Application.fetch_env!(:mv, :rauthy)[:client_id]
- end
- # ... other config
- end
- end
- end
-end
-```
-
-### 3.6 Frontend: Tailwind CSS & DaisyUI
-
-**Utility-First Approach:**
-
-```heex
-
-
-
-
Member Name
-
Email: member@example.com
-
-
-
-
-
-
-
-
-
Member Name
-
-
-```
-
-**Responsive Design:**
-
-```heex
-
-
- <%= for member <- @members do %>
- <.member_card member={member} />
- <% end %>
-
-```
-
-**DaisyUI Components:**
-
-```heex
-
-
-
-
- <.link navigate={~p"/members"} class="btn btn-primary">
- Members
-
-
-
-```
-
-**Custom Tailwind Configuration:**
-
-Update `assets/tailwind.config.js` for custom needs:
-
-```javascript
-module.exports = {
- content: [
- "../lib/mv_web.ex",
- "../lib/mv_web/**/*.*ex"
- ],
- theme: {
- extend: {
- colors: {
- brand: "#FD4F00",
- }
- },
- },
- plugins: [
- require("@tailwindcss/forms"),
- // DaisyUI loaded from vendor
- ]
-}
-```
-
-### 3.7 JavaScript & esbuild
-
-**Minimal JavaScript Philosophy:**
-
-Phoenix LiveView handles most interactivity. Use JavaScript only when necessary:
-
-```javascript
-// assets/js/app.js
-import "phoenix_html"
-import {Socket} from "phoenix"
-import {LiveSocket} from "phoenix_live_view"
-import topbar from "../vendor/topbar"
-
-let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
-let liveSocket = new LiveSocket("/live", Socket, {
- longPollFallbackMs: 2500,
- params: {_csrf_token: csrfToken}
-})
-
-// Show progress bar on live navigation
-topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
-window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
-window.addEventListener("phx:page-loading-stop", _info => topbar.hide())
-
-liveSocket.connect()
-```
-
-**Custom Hooks (when needed):**
-
-```javascript
-let Hooks = {}
-
-Hooks.DatePicker = {
- mounted() {
- // Initialize date picker
- this.el.addEventListener("change", (e) => {
- this.pushEvent("date_selected", {date: e.target.value})
- })
- }
-}
-
-let liveSocket = new LiveSocket("/live", Socket, {
- params: {_csrf_token: csrfToken},
- hooks: Hooks
-})
-```
-
-### 3.8 Code Quality: Credo
-
-**Run Credo Regularly:**
-
-```bash
-# Check code quality
-mix credo
-
-# Strict mode for CI
-mix credo --strict
-```
-
-**Key Credo Checks Enabled:**
-
-- Consistency checks (spacing, line endings, parameter patterns)
-- Design checks (FIXME/TODO tags, alias usage)
-- Readability checks (max line length: 120, module/function names, **module documentation**)
-- Refactoring opportunities (cyclomatic complexity, nesting)
-- Warnings (unused operations, unsafe operations)
-
-**Documentation Enforcement:**
-
-- ✅ `Credo.Check.Readability.ModuleDoc` - **ENABLED** (as of November 2025)
-- All modules require `@moduledoc` documentation
-- Current coverage: 51 @moduledoc declarations across 47 modules (100% core modules)
-- CI pipeline enforces documentation standards
-
-**Address Credo Issues:**
-
-```elixir
-# Before
-def complex_function(user, data, opts) do
- if user.admin? do
- if data.valid? do
- if opts[:force] do
- # deeply nested logic
- end
- end
- end
-end
-
-# After - flatten with guard clauses
-def complex_function(user, _data, _opts) when not user.admin?,
- do: {:error, :unauthorized}
-
-def complex_function(_user, data, _opts) when not data.valid?,
- do: {:error, :invalid_data}
-
-def complex_function(_user, data, opts) do
- if opts[:force] do
- process_data(data)
- else
- validate_and_process(data)
- end
-end
-```
-
-### 3.9 Security: Sobelow
-
-**Run Security Analysis:**
-
-```bash
-# Security audit
-mix sobelow --config
-
-# With verbose output
-mix sobelow --config --verbose
-```
-
-**Security Best Practices:**
-
-- Never commit secrets to version control
-- Use environment variables for sensitive configuration
-- Validate and sanitize all user inputs
-- Use parameterized queries (Ecto handles this)
-- Keep dependencies updated
-
-### 3.10 Dependency Auditing & Updates
-
-**Regular Security Audits:**
-
-```bash
-# Audit dependencies for security vulnerabilities
-mix deps.audit
-
-# Audit hex packages
-mix hex.audit
-
-# Security scan with Sobelow
-mix sobelow --config
-```
-
-**Update Dependencies:**
-
-```bash
-# Update all dependencies
-mix deps.update --all
-
-# Update specific dependency
-mix deps.update phoenix
-
-# Check for outdated packages
-mix hex.outdated
-```
-
-### 3.11 Email: Swoosh
-
-**Mailer Configuration:**
-
-```elixir
-defmodule Mv.Mailer do
- use Swoosh.Mailer, otp_app: :mv
-end
-```
-
-**Sending Emails:**
-
-```elixir
-defmodule Mv.Accounts.WelcomeEmail do
- use Phoenix.Swoosh, template_root: "lib/mv_web/templates"
- import Swoosh.Email
-
- def send(user) do
- new()
- |> to({user.name, user.email})
- |> from({"Mila", "noreply@mila.example.com"})
- |> subject("Welcome to Mila!")
- |> render_body("welcome.html", %{user: user})
- |> Mv.Mailer.deliver()
- end
-end
-```
-
-### 3.12 Internationalization: Gettext
-
-**Define Translations:**
-
-```elixir
-# In LiveView or controller
-gettext("Welcome to Mila")
-
-# With interpolation
-gettext("Hello, %{name}!", name: user.name)
-
-# Domain-specific translations
-dgettext("auth", "Sign in with email")
-```
-
-**Extract and Merge:**
-
-```bash
-# Extract new translatable strings
-mix gettext.extract
-
-# Merge into existing translations
-mix gettext.merge priv/gettext --on-obsolete=mark_as_obsolete
-```
-
-### 3.13 Task Runner: Just
-
-**Common Commands:**
-
-```bash
-# Start development environment
-just run
-
-# Run tests
-just test
-
-# Run linter
-just lint
-
-# Run security audit
-just audit
-
-# Reset database
-just reset-database
-
-# Format code
-just format
-
-# Regenerate migrations
-just regen-migrations migration_name
-```
-
-**Define Custom Tasks:**
-
-Edit `Justfile` for project-specific tasks:
-
-```makefile
-# Example custom task
-setup-dev: install-dependencies start-database migrate-database
- mix phx.gen.secret
- @echo "Development environment ready!"
-```
-
----
-
-## 4. Testing Standards
-
-### 4.1 Test Setup and Organization
-
-**Test Directory Structure:**
-
-Mirror the `lib/` directory structure in `test/`:
-
-```
-test/
-├── accounts/ # Tests for Accounts domain
-│ ├── user_test.exs
-│ ├── email_sync_test.exs
-│ └── ...
-├── membership/ # Tests for Membership domain
-│ ├── member_test.exs
-│ └── ...
-├── mv_web/ # Tests for Web layer
-│ ├── controllers/
-│ ├── live/
-│ └── components/
-└── support/ # Test helpers
- ├── conn_case.ex # Controller test setup
- └── data_case.ex # Database test setup
-```
-
-**Test File Naming:**
-
-- Use `_test.exs` suffix for all test files
-- Match the module name: `user.ex` → `user_test.exs`
-- Use descriptive names for integration tests: `user_member_relationship_test.exs`
-
-### 4.2 ExUnit Basics
-
-**Test Module Structure:**
-
-```elixir
-defmodule Mv.Membership.MemberTest do
- use Mv.DataCase, async: true # async: true for parallel execution
-
- alias Mv.Membership.Member
-
- describe "create_member/1" do
- test "creates a member with valid attributes" do
- attrs = %{
- first_name: "John",
- last_name: "Doe",
- email: "john@example.com"
- }
-
- assert {:ok, %Member{} = member} = Mv.Membership.create_member(attrs)
- assert member.first_name == "John"
- assert member.email == "john@example.com"
- end
-
- test "returns error with invalid attributes" do
- attrs = %{first_name: nil}
-
- assert {:error, _error} = Mv.Membership.create_member(attrs)
- end
- end
-
- describe "list_members/0" do
- setup do
- # Setup code for this describe block
- {:ok, member: create_test_member()}
- end
-
- test "returns all members", %{member: member} do
- members = Mv.Membership.list_members()
- assert length(members) == 1
- assert List.first(members).id == member.id
- end
- end
-end
-```
-
-### 4.3 Test Types
-
-#### 4.3.1 Unit Tests
-
-Test individual functions and modules in isolation:
-
-```elixir
-defmodule Mv.Membership.EmailTest do
- use ExUnit.Case, async: true
-
- alias Mv.Membership.Email
-
- describe "valid?/1" do
- test "returns true for valid email" do
- assert Email.valid?("user@example.com")
- end
-
- test "returns false for invalid email" do
- refute Email.valid?("invalid-email")
- refute Email.valid?("missing-at-sign.com")
- end
- end
-end
-```
-
-#### 4.3.2 Integration Tests
-
-Test interactions between multiple modules or systems:
-
-```elixir
-defmodule Mv.Accounts.UserMemberRelationshipTest do
- use Mv.DataCase, async: true
-
- alias Mv.Accounts.User
- alias Mv.Membership.Member
-
- describe "user-member relationship" do
- test "creating a user automatically creates a member" do
- attrs = %{
- email: "test@example.com",
- password: "SecurePassword123"
- }
-
- assert {:ok, user} = Mv.Accounts.create_user(attrs)
- assert {:ok, member} = Mv.Membership.get_member_by_user_id(user.id)
- assert member.email == user.email
- end
-
- test "deleting a user cascades to member" do
- {:ok, user} = create_user()
- {:ok, member} = Mv.Membership.get_member_by_user_id(user.id)
-
- assert :ok = Mv.Accounts.destroy_user(user)
- assert {:error, :not_found} = Mv.Membership.get_member(member.id)
- end
- end
-end
-```
-
-#### 4.3.3 Controller Tests
-
-Test HTTP endpoints:
-
-```elixir
-defmodule MvWeb.PageControllerTest do
- use MvWeb.ConnCase, async: true
-
- test "GET /", %{conn: conn} do
- conn = get(conn, ~p"/")
- assert html_response(conn, 200) =~ "Welcome to Mila"
- end
-end
-```
-
-#### 4.3.4 LiveView Tests
-
-Test LiveView interactions:
-
-```elixir
-defmodule MvWeb.MemberLive.IndexTest do
- use MvWeb.ConnCase, async: true
-
- import Phoenix.LiveViewTest
-
- setup do
- member = create_test_member()
- %{member: member}
- end
-
- test "displays list of members", %{conn: conn, member: member} do
- {:ok, view, html} = live(conn, ~p"/members")
-
- assert html =~ "Members"
- assert html =~ member.first_name
- end
-
- test "deletes member", %{conn: conn, member: member} do
- {:ok, view, _html} = live(conn, ~p"/members")
-
- assert view
- |> element("#member-#{member.id} a", "Delete")
- |> render_click()
-
- refute has_element?(view, "#member-#{member.id}")
- end
-end
-```
-
-#### 4.3.5 Component Tests
-
-Test function components:
-
-```elixir
-defmodule MvWeb.Components.SearchBarComponentTest do
- use MvWeb.ConnCase, async: true
-
- import Phoenix.LiveViewTest
- import MvWeb.Components.SearchBarComponent
-
- test "renders search input" do
- assigns = %{search_query: "", id: "search"}
-
- html =
- render_component(&search_bar/1, assigns)
-
- assert html =~ "input"
- assert html =~ ~s(type="search")
- end
-end
-```
-
-### 4.4 Test Helpers and Fixtures
-
-**Create Test Helpers:**
-
-```elixir
-# test/support/fixtures.ex
-defmodule Mv.Fixtures do
- def member_fixture(attrs \\ %{}) do
- default_attrs = %{
- first_name: "Test",
- last_name: "User",
- email: "test#{System.unique_integer()}@example.com"
- }
-
- {:ok, member} =
- default_attrs
- |> Map.merge(attrs)
- |> Mv.Membership.create_member()
-
- member
- end
-end
-```
-
-**Use Setup Blocks:**
-
-```elixir
-describe "with authenticated user" do
- setup %{conn: conn} do
- user = create_user()
- conn = log_in_user(conn, user)
- %{conn: conn, user: user}
- end
-
- test "can access protected page", %{conn: conn} do
- conn = get(conn, ~p"/profile")
- assert html_response(conn, 200) =~ "Profile"
- end
-end
-```
-
-### 4.5 Database Testing with Sandbox
-
-**Use Ecto Sandbox for Isolation:**
-
-```elixir
-# test/test_helper.exs
-ExUnit.start()
-Ecto.Adapters.SQL.Sandbox.mode(Mv.Repo, :manual)
-```
-
-```elixir
-# test/support/data_case.ex
-defmodule Mv.DataCase do
- use ExUnit.CaseTemplate
-
- using do
- quote do
- import Ecto
- import Ecto.Changeset
- import Ecto.Query
- import Mv.DataCase
-
- alias Mv.Repo
- end
- end
-
- setup tags do
- pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Mv.Repo, shared: not tags[:async])
- on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
- :ok
- end
-end
-```
-
-### 4.6 Test Coverage
-
-**Run Tests with Coverage:**
-
-```bash
-# Run tests
-mix test
-
-# Run with coverage
-mix test --cover
-
-# Run specific test file
-mix test test/membership/member_test.exs
-
-# Run specific test line
-mix test test/membership/member_test.exs:42
-```
-
-**Coverage Goals:**
-
-- Aim for >80% overall coverage
-- 100% coverage for critical business logic
-- Focus on meaningful tests, not just coverage numbers
-
-### 4.7 Testing Best Practices
-
-**Descriptive Test Names:**
-
-```elixir
-# Good - describes what is being tested
-test "creates a member with valid email address"
-test "returns error when email is already taken"
-test "sends welcome email after successful registration"
-
-# Avoid - vague or generic
-test "member creation"
-test "error case"
-test "test 1"
-```
-
-**Arrange-Act-Assert Pattern:**
-
-```elixir
-test "updates member email" do
- # Arrange - set up test data
- member = member_fixture()
- new_email = "new@example.com"
-
- # Act - perform the action
- {:ok, updated_member} = Mv.Membership.update_member(member, %{email: new_email})
-
- # Assert - verify results
- assert updated_member.email == new_email
-end
-```
-
-**Test One Thing Per Test:**
-
-```elixir
-# Good - focused test
-test "validates email format" do
- attrs = %{email: "invalid-email"}
- assert {:error, _} = Mv.Membership.create_member(attrs)
-end
-
-test "requires email to be present" do
- attrs = %{email: nil}
- assert {:error, _} = Mv.Membership.create_member(attrs)
-end
-
-# Avoid - testing multiple things
-test "validates email" do
- # Tests both format and presence
- assert {:error, _} = Mv.Membership.create_member(%{email: nil})
- assert {:error, _} = Mv.Membership.create_member(%{email: "invalid"})
-end
-```
-
-**Use describe Blocks for Organization:**
-
-```elixir
-describe "create_member/1" do
- test "success case" do
- # ...
- end
-
- test "error case" do
- # ...
- end
-end
-
-describe "update_member/2" do
- test "success case" do
- # ...
- end
-end
-```
-
-**Avoid Testing Implementation Details:**
-
-```elixir
-# Good - test behavior
-test "member can be created with valid attributes" do
- attrs = valid_member_attrs()
- assert {:ok, %Member{}} = Mv.Membership.create_member(attrs)
-end
-
-# Avoid - testing internal implementation
-test "create_member calls Ash.create with correct params" do
- # This is too coupled to implementation
-end
-```
-
-**Keep Tests Fast:**
-
-- Use `async: true` when possible
-- Avoid unnecessary database interactions
-- Mock external services
-- Use fixtures efficiently
-
----
-
-## 5. Security Guidelines
-
-### 5.1 Authentication & Authorization
-
-**Use AshAuthentication:**
-
-```elixir
-# Authentication is configured at the resource level
-authentication do
- strategies do
- password :password do
- identity_field :email
- hashed_password_field :hashed_password
- end
-
- oauth2 :rauthy do
- # OIDC configuration
- end
- end
-end
-```
-
-**Implement Authorization Policies:**
-
-```elixir
-policies do
- # Default deny
- policy action_type(:*) do
- authorize_if always()
- end
-
- # Specific permissions
- policy action_type([:read, :update]) do
- authorize_if relates_to_actor_via(:user)
- end
-
- policy action_type(:destroy) do
- authorize_if actor_attribute_equals(:role, :admin)
- end
-end
-```
-
-### 5.2 Password Security
-
-**Use bcrypt for Password Hashing:**
-
-```elixir
-# Configured in AshAuthentication resource
-password :password do
- identity_field :email
- hashed_password_field :hashed_password
- hash_provider AshAuthentication.BcryptProvider
-
- confirmation_required? true
-end
-```
-
-**Password Requirements:**
-
-- Minimum 12 characters
-- Mix of uppercase, lowercase, numbers (enforced by validation)
-- Use `bcrypt_elixir` for hashing (never store plain text passwords)
-
-### 5.3 Input Validation & Sanitization
-
-**Validate All User Input:**
-
-```elixir
-attributes do
- attribute :email, :string do
- allow_nil? false
- public? true
- end
-end
-
-validations do
- validate present(:email)
- validate match(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/)
-end
-```
-
-**SQL Injection Prevention:**
-
-Ecto and Ash handle parameterized queries automatically:
-
-```elixir
-# Safe - parameterized query
-Ash.Query.filter(Member, email == ^user_email)
-
-# Avoid raw SQL when possible
-Ecto.Adapters.SQL.query(Mv.Repo, "SELECT * FROM members WHERE email = $1", [user_email])
-```
-
-### 5.4 CSRF Protection
-
-**Phoenix Handles CSRF Automatically:**
-
-```heex
-
-<.form for={@form} phx-submit="save">
-
-
-```
-
-**Configure in Endpoint:**
-
-```elixir
-# lib/mv_web/endpoint.ex
-plug Plug.Session,
- store: :cookie,
- key: "_mv_key",
- signing_salt: "secret"
-
-plug :protect_from_forgery
-```
-
-### 5.5 Secrets Management
-
-**Never Commit Secrets:**
-
-```bash
-# .gitignore should include:
-.env
-.env.*
-!.env.example
-```
-
-**Use Environment Variables:**
-
-```elixir
-# config/runtime.exs
-config :mv, :rauthy,
- client_id: System.get_env("OIDC_CLIENT_ID") || "mv",
- client_secret: System.get_env("OIDC_CLIENT_SECRET"),
- base_url: System.get_env("OIDC_BASE_URL")
-```
-
-**Generate Secure Secrets:**
-
-```bash
-# Generate secret key base
-mix phx.gen.secret
-
-# Generate token signing secret
-mix phx.gen.secret
-```
-
-### 5.6 Security Headers
-
-**Configure Security Headers:**
-
-```elixir
-# lib/mv_web/endpoint.ex
-plug Plug.Static,
- at: "/",
- from: :mv,
- gzip: false,
- only: MvWeb.static_paths(),
- headers: %{
- "x-content-type-options" => "nosniff",
- "x-frame-options" => "SAMEORIGIN",
- "x-xss-protection" => "1; mode=block"
- }
-```
-
-### 5.7 Dependency Security
-
-- **Use Renovate for automated dependency updates**
-- **Review changelogs before updating dependencies**
-- **Test thoroughly after updates**
-- **Run regular audits** (see section 3.10 for audit commands)
-
-### 5.8 Logging & Monitoring
-
-**Sanitize Logs:**
-
-```elixir
-# Don't log sensitive information
-Logger.info("User login attempt", user_id: user.id)
-
-# Avoid
-Logger.info("User login attempt", user: inspect(user)) # May contain password
-```
-
-**Configure Logger:**
-
-```elixir
-# config/config.exs
-config :logger, :default_formatter,
- format: "$time $metadata[$level] $message\n",
- metadata: [:request_id, :user_id]
-```
-
----
-
-## 6. Performance Best Practices
-
-### 6.1 Database Performance
-
-**Indexing:**
-
-```elixir
-postgres do
- table "members"
- repo Mv.Repo
-
- # Add indexes for frequently queried fields
- index [:email], unique: true
- index [:last_name]
- index [:created_at]
-end
-```
-
-**Avoid N+1 Queries:**
-
-```elixir
-# Good - preload relationships
-members =
- Member
- |> Ash.Query.load([:properties, :user])
- |> Mv.Membership.list_members!()
-
-# Avoid - causes N+1
-members = Mv.Membership.list_members!()
-Enum.map(members, fn member ->
- properties = Ash.load!(member, :properties) # N queries!
-end)
-```
-
-**Pagination:**
-
-```elixir
-# Use keyset pagination (configured as default in Ash)
-Ash.Query.page(Member, offset: 0, limit: 50)
-```
-
-**Batch Operations:**
-
-```elixir
-# Use bulk operations for multiple records
-Ash.bulk_create([member1_attrs, member2_attrs, member3_attrs], Member, :create)
-```
-
-### 6.2 LiveView Performance
-
-**Optimize Assigns:**
-
-```elixir
-# Good - only assign what's needed
-def mount(_params, _session, socket) do
- {:ok, assign(socket, members_count: get_count())}
-end
-
-# Avoid - assigning large collections unnecessarily
-def mount(_params, _session, socket) do
- {:ok, assign(socket, all_members: list_all_members())} # Heavy!
-end
-```
-
-**Use Temporary Assigns:**
-
-```elixir
-# For data that's only needed for rendering
-def handle_event("load_report", _, socket) do
- report_data = generate_large_report()
- {:noreply, assign(socket, report: report_data) |> assign(:report, temporary_assigns: [:report])}
-end
-```
-
-**Stream Collections:**
-
-```elixir
-# For large collections
-def mount(_params, _session, socket) do
- {:ok, stream(socket, :members, list_members())}
-end
-
-def render(assigns) do
- ~H"""
-
-
- <%= member.name %>
-
-
- """
-end
-```
-
-### 6.3 Caching Strategies
-
-**Function-Level Caching:**
-
-```elixir
-defmodule Mv.Cache do
- use GenServer
-
- def get_or_compute(key, compute_fn) do
- case get(key) do
- nil ->
- value = compute_fn.()
- put(key, value)
- value
-
- value ->
- value
- end
- end
-end
-```
-
-**ETS for In-Memory Cache:**
-
-```elixir
-# In application.ex
-:ets.new(:mv_cache, [:named_table, :public, read_concurrency: true])
-
-# Usage
-:ets.insert(:mv_cache, {"key", "value"})
-:ets.lookup(:mv_cache, "key")
-```
-
-### 6.4 Async Processing
-
-**Background Jobs (Future):**
-
-```elixir
-# When Oban is added
-defmodule Mv.Workers.EmailWorker do
- use Oban.Worker
-
- @impl Oban.Worker
- def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
- user = Mv.Accounts.get_user!(user_id)
- Mv.Mailer.send_welcome_email(user)
- :ok
- end
-end
-
-# Enqueue job
-%{user_id: user.id}
-|> Mv.Workers.EmailWorker.new()
-|> Oban.insert()
-```
-
-**Task Async for Concurrent Operations:**
-
-```elixir
-def gather_dashboard_data do
- tasks = [
- Task.async(fn -> get_member_count() end),
- Task.async(fn -> get_recent_registrations() end),
- Task.async(fn -> get_payment_status() end)
- ]
-
- [member_count, recent, payments] = Task.await_many(tasks)
-
- %{
- member_count: member_count,
- recent: recent,
- payments: payments
- }
-end
-```
-
-### 6.5 Profiling & Monitoring
-
-**Use :observer:**
-
-```elixir
-# In iex session
-:observer.start()
-```
-
-**Use Telemetry:**
-
-```elixir
-# Attach telemetry handlers
-:telemetry.attach(
- "query-duration",
- [:mv, :repo, :query],
- fn event, measurements, metadata, _config ->
- Logger.info("Query took #{measurements.total_time}ms")
- end,
- nil
-)
-```
-
----
-
-## 7. Documentation Standards
-
-### 7.1 Module Documentation
-
-**Use @moduledoc:**
-
-```elixir
-defmodule Mv.Membership.Member do
- @moduledoc """
- Represents a club member with their personal information and membership status.
-
- Members can have custom properties defined by the club administrators.
- Each member is optionally linked to a user account for self-service access.
-
- ## Examples
-
- iex> Mv.Membership.create_member(%{first_name: "John", last_name: "Doe", email: "john@example.com"})
- {:ok, %Mv.Membership.Member{}}
-
- """
-end
-```
-
-**Module Documentation Should Include:**
-
-- Purpose of the module
-- Key responsibilities
-- Usage examples
-- Related modules
-
-### 7.2 Function Documentation
-
-**Use @doc:**
-
-```elixir
-@doc """
-Creates a new member with the given attributes.
-
-## Parameters
-
- - `attrs` - A map of member attributes including:
- - `:first_name` (required) - The member's first name
- - `:last_name` (required) - The member's last name
- - `:email` (required) - The member's email address
-
-## Returns
-
- - `{:ok, member}` - Successfully created member
- - `{:error, error}` - Validation or creation error
-
-## Examples
-
- iex> Mv.Membership.create_member(%{
- ...> first_name: "Jane",
- ...> last_name: "Smith",
- ...> email: "jane@example.com"
- ...> })
- {:ok, %Mv.Membership.Member{first_name: "Jane"}}
-
- iex> Mv.Membership.create_member(%{first_name: nil})
- {:error, %Ash.Error.Invalid{}}
-
-"""
-@spec create_member(map()) :: {:ok, Member.t()} | {:error, Ash.Error.t()}
-def create_member(attrs) do
- # Implementation
-end
-```
-
-### 7.3 Type Specifications
-
-**Use @spec for Function Signatures:**
-
-```elixir
-@spec create_member(map()) :: {:ok, Member.t()} | {:error, Ash.Error.t()}
-def create_member(attrs)
-
-@spec list_members(keyword()) :: [Member.t()]
-def list_members(opts \\ [])
-
-@spec get_member!(String.t()) :: Member.t() | no_return()
-def get_member!(id)
-```
-
-### 7.4 Code Comments
-
-**When to Comment:**
-
-```elixir
-# Good - explain WHY, not WHAT
-def calculate_dues(member) do
- # Annual dues are prorated based on join date to be fair to mid-year joiners
- months_active = calculate_months_active(member)
- @annual_dues * (months_active / 12)
-end
-
-# Avoid - stating the obvious
-def calculate_dues(member) do
- # Calculate the dues
- months_active = calculate_months_active(member) # Get months active
- @annual_dues * (months_active / 12) # Multiply annual dues by fraction
-end
-```
-
-**Complex Logic:**
-
-```elixir
-def sync_member_email(member, user) do
- # Email synchronization priority:
- # 1. User email is the source of truth (authenticated account)
- # 2. Member email is updated to match user email
- # 3. Preserve member email history for audit purposes
-
- if member.email != user.email do
- # Archive old email before updating
- archive_member_email(member, member.email)
- update_member(member, %{email: user.email})
- end
-end
-```
-
-### 7.5 README and Project Documentation
-
-**Keep README.md Updated:**
-
-- Installation instructions
-- Development setup
-- Running tests
-- Deployment guide
-- Contributing guidelines
-
-**Additional Documentation:**
-
-- `docs/` directory for detailed guides
-- Architecture decision records (ADRs)
-- API documentation (generated with ExDoc)
-
-**Generate Documentation:**
-
-```bash
-# Generate HTML documentation
-mix docs
-
-# View documentation
-open doc/index.html
-```
-
-### 7.6 Changelog
-
-**Maintain CHANGELOG.md:**
-
-```markdown
-# Changelog
-
-## [Unreleased]
-
-### Added
-- Member custom properties feature
-- Email synchronization between user and member
-
-### Changed
-- Updated Phoenix to 1.8.0
-
-### Fixed
-- Email uniqueness validation bug
-
-## [0.1.0] - 2025-01-15
-
-### Added
-- Initial release
-- Basic member management
-- OIDC authentication
-```
-
----
-
-## 8. Git Workflow
-
-### 8.1 Branching Strategy
-
-**Main Branches:**
-
-- `main` - Production-ready code
-
-**Feature Branches:**
-
-```bash
-# Create feature branch
-git checkout -b feature/member-custom-properties
-
-# Work on feature
-git add .
-git commit -m "Add custom properties to members"
-
-# Push to remote
-git push origin feature/member-custom-properties
-```
-
-### 8.2 Commit Messages
-
-**Format:**
-
-```
-:
-
-
-
-