diff --git a/.credo.exs b/.credo.exs
index d871572..4eddee8 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, []}
+ {Credo.Check.Warning.WrongTestFileExtension, []},
+ # Module documentation check (enabled after adding @moduledoc to all modules)
+ {Credo.Check.Readability.ModuleDoc, []}
],
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
new file mode 100644
index 0000000..4a82edb
--- /dev/null
+++ b/CODE_GUIDELINES.md
@@ -0,0 +1,2578 @@
+# 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:**
+
+```
+:
+
+
+
+