# `Accrue.Config`
[🔗](https://github.com/szTheory/accrue/blob/accrue-v0.3.0/lib/accrue/config.ex#L1)

Runtime configuration schema for Accrue, backed by `NimbleOptions`.

This module is the **single source of truth** for supported `:accrue`
application keys. Host code reads validated values via `get!/1` or
`Application.get_env/3`; extend behaviour through adapters, not by editing
this schema from application code.

## Compile-time vs runtime

Adapter atoms (`:processor`, `:mailer`, `:mailer_adapter`, `:pdf_adapter`,
`:auth_adapter`) are stable per-deploy and fine at compile time via
`Application.compile_env!/2`.

Secrets (`:stripe_secret_key`) and host-owned fields (`:default_currency`,
`:from_email`, brand colors) MUST be read at runtime. See CLAUDE.md
§Config Boundaries.

## Options

* `:repo` (`t:atom/0`) - Required. Host `Ecto.Repo` module that Accrue writes to (event ledger, webhook events, billing tables).

* `:processor` (`t:atom/0`) - Processor adapter implementing `Accrue.Processor` behaviour. The default value is `Accrue.Processor.Fake`.

* `:mailer` (`t:atom/0`) - Mailer pipeline module implementing `Accrue.Mailer` behaviour. The default value is `Accrue.Mailer.Default`.

* `:mailer_adapter` (`t:atom/0`) - Swoosh-backed mailer delivery module. The default value is `Accrue.Mailer.Swoosh`.

* `:pdf_adapter` (`t:atom/0`) - PDF adapter implementing `Accrue.PDF` behaviour. The default value is `Accrue.PDF.ChromicPDF`.

* `:auth_adapter` (`t:atom/0`) - Auth adapter implementing `Accrue.Auth` behaviour. The default value is `Accrue.Auth.Default`.

* `:storage_adapter` (`t:atom/0`) - Storage adapter implementing `Accrue.Storage` behaviour. v1.0 ships `Accrue.Storage.Null` only; hosts supply a custom adapter (e.g., S3) to enable persisted asset storage. `Accrue.Storage.Filesystem` ships in v1.1. The default value is `Accrue.Storage.Null`.

* `:stripe_secret_key` (`t:String.t/0`) - Runtime Stripe secret key. MUST be read at runtime only; never via `Application.compile_env!/2`. Validated at boot when `processor == Accrue.Processor.Stripe`.

* `:stripe_api_version` (`t:String.t/0`) - Stripe API version pinned by the `:lattice_stripe` wrapper. The default value is `"2026-03-25.dahlia"`.

* `:emails` (`t:keyword/0`) - Per-email-type switches. Keys are email type atoms; values are `boolean` or `{Mod, :fun, args}` MFA callbacks. The default value is `[]`.

* `:email_overrides` (`t:keyword/0`) - Per-email-type template module overrides (third rung of the override ladder; see `guides/email.md`). Keys are email type atoms; values are module names. The default value is `[]`.

* `:attach_invoice_pdf` (`t:boolean/0`) - Auto-attach invoice PDF to the receipt email. The default value is `true`.

* `:enforce_immutability` (`t:boolean/0`) - When true, `Accrue.Application` boot raises if the current PG role has UPDATE/DELETE on `accrue_events`. The default value is `false`.

* `:business_name` (`t:String.t/0`) - Business name shown in email headers, PDFs, and admin UI. The default value is `"Accrue"`.

* `:business_address` (`t:String.t/0`) - Business postal address shown in invoice footers. The default value is `""`.

* `:logo_url` (`t:String.t/0`) - Absolute URL to the brand logo used in email + PDF headers. The default value is `""`.

* `:support_email` (`t:String.t/0`) - Reply-to support email address for transactional mail. The default value is `"support@example.com"`.

* `:from_email` (`t:String.t/0`) - Default From: address for transactional mail. The default value is `"noreply@example.com"`.

* `:from_name` (`t:String.t/0`) - Default From: name for transactional mail. The default value is `"Accrue"`.

* `:default_currency` (`t:atom/0`) - Default currency when one is not explicitly supplied. The default value is `:usd`.

* `:webhook_signing_secrets` (`t:term/0`) - Map of processor atom to signing secret(s). Each value is a string or list of strings for rotation. Example: `%{stripe: ["whsec_old", "whsec_new"]}`. The default value is `%{}`.

* `:succeeded_retention_days` - Number of days to retain `:succeeded` webhook events before the Pruner deletes them. Set to `:infinity` to disable pruning. Default: 14. The default value is `14`.

* `:dead_retention_days` - Number of days to retain `:dead` webhook events before the Pruner deletes them. Set to `:infinity` to disable pruning. Default: 90. The default value is `90`.

* `:webhook_handlers` (list of `t:atom/0`) - List of modules implementing `Accrue.Webhook.Handler` behaviour. Called sequentially after the default handler on each webhook event. Example: `[MyApp.BillingHandler, MyApp.AnalyticsHandler]`. The default value is `[]`.

* `:expiring_card_thresholds` - Strictly-descending list of day thresholds at which the expiring-card reminder email fires ahead of a stored card's expiration. Default: `[30, 7, 1]` — 30, 7, and 1 days out. The default value is `[30, 7, 1]`.

* `:idempotency_mode` - How `Accrue.Actor.current_operation_id!/0` behaves when the process dict has no operation_id. `:strict` raises `Accrue.ConfigError`; `:warn` (the default) generates a random UUID and logs a warning. Set to `:strict` in production to ensure every outbound processor call carries a deterministic idempotency key. The default value is `:warn`.

* `:succeeded_refund_retention_days` (`t:pos_integer/0`) - Number of days to retain `:succeeded` refund records before pruning Default: 90. The default value is `90`.

* `:dunning` (`t:keyword/0`) - Dunning grace-period overlay config. `:mode` is `:stripe_smart_retries` or `:disabled`; `:terminal_action` is `:unpaid` or `:canceled`; `:grace_days` adds N days past Stripe's last retry before Accrue asks the processor facade to move the subscription to the terminal action. The default value is `[mode: :stripe_smart_retries, grace_days: 14, terminal_action: :unpaid, telemetry_prefix: [:accrue, :ops]]`.

* `:webhook_endpoints` (`t:keyword/0`) - Map of endpoint name to `[secret:, mode:]` for multi-endpoint webhooks. Example: `[primary: [secret: "whsec_..."], connect: [secret: "whsec_...", mode: :connect]]`. The default value is `[]`.

* `:dlq_replay_batch_size` (`t:pos_integer/0`) - Number of rows per chunk in `Accrue.Webhooks.DLQ.requeue_where/2` bulk replay. The default value is `100`.

* `:dlq_replay_stagger_ms` (`t:non_neg_integer/0`) - Milliseconds to sleep between chunks during DLQ bulk replay (protects downstream). Default: 1_000. The default value is `1000`.

* `:dlq_replay_max_rows` (`t:pos_integer/0`) - Hard cap on bulk replay. Returns `{:error, :replay_too_large}` unless `force: true` is passed. Default: 10_000. The default value is `10000`.

* `:branding` (`t:keyword/0`) - Branding config. Single source of truth for email + PDF brand. `:from_email` and `:support_email` are required for any real deploy. See guides/branding.md. The default value is `[]`.

  * `:business_name` (`t:String.t/0`) - The default value is `"Accrue"`.

  * `:from_name` (`t:String.t/0`) - The default value is `"Accrue"`.

  * `:from_email` (`t:String.t/0`) - Required.

  * `:support_email` (`t:String.t/0`) - Required.

  * `:reply_to_email` - The default value is `nil`.

  * `:logo_url` - The default value is `nil`.

  * `:logo_dark_url` - The default value is `nil`.

  * `:accent_color` - The default value is `"#1F6FEB"`.

  * `:secondary_color` - The default value is `"#6B7280"`.

  * `:font_stack` (`t:String.t/0`) - The default value is `"-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif"`.

  * `:company_address` - The default value is `nil`.

  * `:support_url` - The default value is `nil`.

  * `:social_links` (`t:keyword/0`) - The default value is `[]`.

  * `:list_unsubscribe_url` - The default value is `nil`.

* `:default_locale` (`t:String.t/0`) - Application-wide default locale for email + PDF rendering. Third rung of the locale precedence ladder (after assigns[:locale] and customer.preferred_locale). Bad locales fall back to "en". The default value is `"en"`.

* `:default_timezone` (`t:String.t/0`) - Application-wide default IANA timezone for datetime rendering. Third rung of the timezone precedence ladder (after assigns[:timezone] and customer.preferred_timezone). Bad zones fall back to "Etc/UTC". The default value is `"Etc/UTC"`.

* `:cldr_backend` (`t:atom/0`) - Cldr backend module used by `Accrue.Workers.Mailer.enrich/2` to validate locale strings. Defaults to `Accrue.Cldr`. The default value is `Accrue.Cldr`.

* `:connect` (`t:keyword/0`) - Stripe Connect configuration. `:default_stripe_account` is the fallback connected account id used when no per-call override or pdict scope is active (three-level precedence chain). `:platform_fee` configures the default flat-rate fee consumed by `Accrue.Connect.platform_fee/2`: `:percent` is a `Decimal` percentage (e.g. `Decimal.new("2.9")` for 2.9%), `:fixed` is an `Accrue.Money` fee in minor units added after the percentage, and `:min`/`:max` optionally clamp the result. The default value is `[default_stripe_account: nil, platform_fee: [percent: Decimal.new("2.9"), fixed: nil, min: nil, max: nil]]`.

# `branding`

```elixir
@spec branding() :: keyword()
```

Returns the branding config keyword list.

Falls back to building a keyword list from deprecated top-level flat
branding keys (`:business_name`, `:logo_url`, `:from_email`,
`:from_name`, `:support_email`, `:business_address`) when the nested
`:branding` key is unset or empty. Nested `:branding` always takes
precedence. See `Accrue.Application.warn_deprecated_branding/0` for the
boot-time `Logger.warning` when flat keys are still in use.

# `branding`

```elixir
@spec branding(atom()) :: term()
```

Returns a single branding key. Raises if the key is unknown.

# `cldr_backend`

```elixir
@spec cldr_backend() :: module()
```

Returns the configured Cldr backend module used by
`Accrue.Workers.Mailer.enrich/2` to validate locale strings.

# `connect`

```elixir
@spec connect() :: keyword()
```

Returns the Connect config keyword list.

Shape: `[default_stripe_account: String.t() | nil,
          platform_fee: [percent: Decimal.t(), fixed: Accrue.Money.t() | nil,
                         min: Accrue.Money.t() | nil, max: Accrue.Money.t() | nil]]`.

# `dead_retention_days`

```elixir
@spec dead_retention_days() :: pos_integer() | :infinity
```

Returns the number of days to retain `:dead` webhook events.

# `default_locale`

```elixir
@spec default_locale() :: String.t()
```

Returns the application default locale string.

# `default_timezone`

```elixir
@spec default_timezone() :: String.t()
```

Returns the application default IANA timezone string.

# `deprecated_flat_branding_keys`

```elixir
@spec deprecated_flat_branding_keys() :: [atom()]
```

Returns the list of deprecated flat branding keys.
Consumed by `Accrue.Application.warn_deprecated_branding/0` and by the
internal flat-key shim in `branding/0`.

# `dlq_replay_batch_size`

```elixir
@spec dlq_replay_batch_size() :: pos_integer()
```

Returns the DLQ bulk-replay chunk size.

# `dlq_replay_max_rows`

```elixir
@spec dlq_replay_max_rows() :: pos_integer()
```

Returns the hard cap on DLQ bulk-replay rows.

# `dlq_replay_stagger_ms`

```elixir
@spec dlq_replay_stagger_ms() :: non_neg_integer()
```

Returns the DLQ bulk-replay inter-chunk stagger in milliseconds.

# `dunning`

```elixir
@spec dunning() :: keyword()
```

Returns the dunning grace-period overlay config.

# `get!`

```elixir
@spec get!(atom()) :: term()
```

Reads a config key from `Application.get_env/3`, falling back to the
schema default. Raises `Accrue.ConfigError` if the key is not in the
schema at all (prevents silent typos in downstream code).

# `schema`

```elixir
@spec schema() :: keyword()
```

Returns the NimbleOptions schema keyword list. Used by boot-time
validation to iterate keys.

# `stripe_api_version`

```elixir
@spec stripe_api_version() :: String.t()
```

Returns the configured Stripe API version string.

# `succeeded_retention_days`

```elixir
@spec succeeded_retention_days() :: pos_integer() | :infinity
```

Returns the number of days to retain `:succeeded` webhook events.

# `validate!`

```elixir
@spec validate!(keyword()) :: keyword()
```

Validates a keyword list against the Accrue config schema and returns the
normalized form. Raises `NimbleOptions.ValidationError` on failure.

# `validate_at_boot!`

```elixir
@spec validate_at_boot!() :: :ok
```

Reads the current `:accrue` application env at boot time, filters it to
the schema-known keys, and validates via `NimbleOptions.validate!/2`.

Called by `Accrue.Application.start/2` before the supervision tree
starts. Raises `NimbleOptions.ValidationError` on misconfig — fail loud
rather than limp into production with silently-broken config.

Only schema-known keys are validated. Extra keys in the `:accrue` env
(e.g., per-module adapter configs like `Accrue.Mailer.Swoosh`) are
ignored here — they belong to their own libraries and would otherwise
produce spurious `unknown option` errors.

# `validate_descending`

```elixir
@spec validate_descending(term()) :: {:ok, [pos_integer()]} | {:error, String.t()}
```

NimbleOptions `:custom` validator for `:expiring_card_thresholds`.

Accepts a non-empty list of positive integers that is strictly
descending (each element strictly less than the previous). Returns
`{:ok, list}` on success, `{:error, message}` on failure.

# `validate_hex`

```elixir
@spec validate_hex(term()) :: {:ok, String.t()} | {:error, String.t()}
```

NimbleOptions `:custom` validator for `:branding.accent_color` /
`:branding.secondary_color`. Accepts `#rgb`, `#rrggbb`, and
`#rrggbbaa` hex color strings; rejects anything else.

# `webhook_endpoints`

```elixir
@spec webhook_endpoints() :: keyword()
```

Returns the multi-endpoint webhook config.

# `webhook_handlers`

```elixir
@spec webhook_handlers() :: [module()]
```

Returns the list of user-registered webhook handler modules.

# `webhook_signing_secrets`

```elixir
@spec webhook_signing_secrets(atom()) :: String.t() | [String.t()]
```

Returns the signing secret(s) for the given processor.

Looks up `webhook_signing_secrets` in the `:accrue` application env
and extracts the value for the given processor atom. Returns a list
of strings (for multi-secret rotation support). Raises
`Accrue.ConfigError` if no secrets are configured for the processor.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
