# Rails `Provider` Generator Guide

This guide explains how to use the `Provider` generators, which make it easy to add new
integrations with either **global** or **per-family** scope credentials.

## Table of Contents
1. [Quick Start](#quick-start)
2. [Global vs Per-Family: Which to Use?](#global-vs-per-family-which-to-use)
3. [Provider:Family Generator](#providerfamily-generator)
4. [Provider:Global Generator](#providerglobal-generator)
5. [Comparison Table](#comparison-table)
6. [Examples](#examples)

---

## Quick Start

### Two Generators Available

```bash
# Per-Family Provider (each family has separate credentials)
rails g provider:family <PROVIDER_NAME> field:type[:secret] ...

# Global Provider (all families share site-wide credentials)
rails g provider:global <PROVIDER_NAME> field:type[:secret] ...
```

### Quick Examples

```bash
# Per-family: Each family has their own Lunchflow API key
rails g provider:family lunchflow api_key:text:secret base_url:string

# Global: All families share the same Plaid credentials
rails g provider:global plaid \
  client_id:string:secret \
  secret:string:secret \
  environment:string:default=sandbox
```

---

## Global vs Per-Family: Which to Use?

### Use `provider:global` When:
- ✅ One set of credentials serves the entire application
- ✅ Provider charges per-application (not per-customer)
- ✅ You control the API account (self-hosted or managed mode)
- ✅ All families can safely share access
- ✅ Examples: Plaid, OpenAI, exchange rate services

### Use `provider:family` When:
- ✅ Each family/customer needs their own credentials
- ✅ Provider charges per-customer
- ✅ Users bring their own API keys
- ✅ Data isolation required between families
- ✅ Examples: Lunch Flow, SimpleFIN, YNAB, personal bank APIs

---

## Provider:Family Generator

### Usage

```bash
rails g provider:family <PROVIDER_NAME> field:type[:secret][:default=value] ...
```

### Example: Adding a MyBank Provider

```bash
rails g provider:family my_bank \
  api_key:text:secret \
  base_url:string:default=https://api.mybank.com \
  refresh_token:text:secret
```

### What Gets Generated

This single command generates:
- ✅ Migration for `my_bank_items` and `my_bank_accounts` tables **with credential fields**
- ✅ Models: `MyBankItem`, `MyBankAccount`, and `MyBankItem::Provided` concern
- ✅ Adapter class for provider integration
- ✅ Simple manual panel view for provider settings
- ✅ Controller with CRUD actions and Turbo Stream support
- ✅ Routes
- ✅ Updates to settings controller and view

### Key Characteristics
- **Credentials**: Stored in `my_bank_items` table (encrypted)
- **Isolation**: Each family has completely separate credentials
- **UI**: Manual form panel at `/settings/providers`
- **Configuration**: Per-family, self-service

---

## Provider:Global Generator

### Usage

```bash
rails g provider:global <PROVIDER_NAME> field:type[:secret][:default=value] ...
```

### Example: Adding a Plaid Provider

```bash
rails g provider:global plaid \
  client_id:string:secret \
  secret:string:secret \
  environment:string:default=sandbox
```

### What Gets Generated

This single command generates:
- ✅ Migration for `plaid_items` and `plaid_accounts` tables **without credential fields**
- ✅ Models: `PlaidItem`, `PlaidAccount`, and `PlaidItem::Provided` concern
- ✅ Adapter with `Provider::Configurable`
- ❌ No controller (credentials managed globally)
- ❌ No view (UI auto-generated by `Provider::Configurable`)
- ❌ No routes (no CRUD needed)

### Key Characteristics
- **Credentials**: Stored in `settings` table (global, not encrypted)
- **Sharing**: All families use the same credentials
- **UI**: Auto-generated at `/settings/providers` (self-hosted mode only)
- **Configuration**: ENV variables or admin settings

### Important Notes
- Credentials are shared by **all families** - use only for trusted services
- Only available in **self-hosted mode** (admin-only access)
- No per-family credential management needed
- Simpler implementation (fewer files generated)

---

## Comparison Table

| Aspect | `provider:family` | `provider:global` |
|--------|------------------|------------------|
| **Credentials storage** | `provider_items` table (per-family) | `settings` table (global) |
| **Credential encryption** | ✅ Yes (ActiveRecord encryption) | ❌ No (plaintext in settings) |
| **Family isolation** | ✅ Complete (each family has own credentials) | ❌ None (all families share) |
| **Files generated** | 9+ files | 5 files |
| **Migration includes credentials** | ✅ Yes | ❌ No |
| **Controller** | ✅ Yes (simple CRUD) | ❌ No |
| **View** | ✅ Manual form panel | ❌ Auto-generated |
| **Routes** | ✅ Yes | ❌ No |
| **UI location** | `/settings/providers` (always) | `/settings/providers` (self-hosted only) |
| **ENV variable support** | ❌ No (per-family can't use ENV) | ✅ Yes (fallback) |
| **Use case** | User brings own API key | Platform provides API access |
| **Examples** | Lunch Flow, SimpleFIN, YNAB | Plaid, OpenAI, TwelveData |

---

## What Gets Generated (Detailed)

### 1. Migration

**File:** `db/migrate/xxx_create_my_bank_tables_and_accounts.rb`

Creates two complete tables with all necessary fields:

```ruby
class CreateMyBankTablesAndAccounts < ActiveRecord::Migration[7.2]
  def change
    # Create provider items table (stores per-family connection credentials)
    create_table :my_bank_items, id: :uuid do |t|
      t.references :family, null: false, foreign_key: true, type: :uuid
      t.string :name

      # Institution metadata
      t.string :institution_id
      t.string :institution_name
      t.string :institution_domain
      t.string :institution_url
      t.string :institution_color

      # Status and lifecycle
      t.string :status, default: "good"
      t.boolean :scheduled_for_deletion, default: false
      t.boolean :pending_account_setup, default: false

      # Sync settings
      t.datetime :sync_start_date

      # Raw data storage
      t.jsonb :raw_payload
      t.jsonb :raw_institution_payload

      # Provider-specific credential fields
      t.text :api_key
      t.string :base_url
      t.text :refresh_token

      t.timestamps
    end

    add_index :my_bank_items, :family_id
    add_index :my_bank_items, :status

    # Create provider accounts table (stores individual account data from provider)
    create_table :my_bank_accounts, id: :uuid do |t|
      t.references :my_bank_item, null: false, foreign_key: true, type: :uuid

      # Account identification
      t.string :name
      t.string :account_id

      # Account details
      t.string :currency
      t.decimal :current_balance, precision: 19, scale: 4
      t.string :account_status
      t.string :account_type
      t.string :provider

      # Metadata and raw data
      t.jsonb :institution_metadata
      t.jsonb :raw_payload
      t.jsonb :raw_transactions_payload

      t.timestamps
    end

    add_index :my_bank_accounts, :account_id
    add_index :my_bank_accounts, :my_bank_item_id
  end
end
```

### 2. Models

**File:** `app/models/my_bank_item.rb`

The item model stores per-family connection credentials:

```ruby
class MyBankItem < ApplicationRecord
  include Syncable, Provided

  enum :status, { good: "good", requires_update: "requires_update" }, default: :good

  # Encryption for secret fields
  if Rails.application.credentials.active_record_encryption.present?
    encrypts :api_key, :refresh_token, deterministic: true
  end

  validates :name, presence: true
  validates :api_key, presence: true, on: :create
  validates :refresh_token, presence: true, on: :create

  belongs_to :family
  has_one_attached :logo
  has_many :my_bank_accounts, dependent: :destroy
  has_many :accounts, through: :my_bank_accounts

  scope :active, -> { where(scheduled_for_deletion: false) }
  scope :ordered, -> { order(created_at: :desc) }
  scope :needs_update, -> { where(status: :requires_update) }

  def destroy_later
    update!(scheduled_for_deletion: true)
    DestroyJob.perform_later(self)
  end

  def credentials_configured?
    api_key.present? && refresh_token.present?
  end

  def effective_base_url
    base_url.presence || "https://api.mybank.com"
  end
end
```

**File:** `app/models/my_bank_account.rb`

The account model stores individual account data from the provider:

```ruby
class MyBankAccount < ApplicationRecord
  include CurrencyNormalizable

  belongs_to :my_bank_item

  # Association through account_providers for linking to internal accounts
  has_one :account_provider, as: :provider, dependent: :destroy
  has_one :account, through: :account_provider, source: :account

  validates :name, :currency, presence: true

  def upsert_my_bank_snapshot!(account_snapshot)
    update!(
      current_balance: account_snapshot[:balance],
      currency: parse_currency(account_snapshot[:currency]) || "USD",
      name: account_snapshot[:name],
      account_id: account_snapshot[:id]&.to_s,
      account_status: account_snapshot[:status],
      raw_payload: account_snapshot
    )
  end
end
```

**File:** `app/models/my_bank_item/provided.rb`

The Provided concern connects the item to its provider SDK:

```ruby
module MyBankItem::Provided
  extend ActiveSupport::Concern

  def my_bank_provider
    return nil unless credentials_configured?

    Provider::MyBank.new(
      api_key,
      base_url: effective_base_url,
      refresh_token: refresh_token
    )
  end
end
```

### 3. Adapter

**File:** `app/models/provider/my_bank_adapter.rb`

```ruby
class Provider::MyBankAdapter < Provider::Base
  include Provider::Syncable
  include Provider::InstitutionMetadata

  # Register this adapter with the factory
  Provider::Factory.register("MyBankAccount", self)

  def provider_name
    "my_bank"
  end

  # Build a My Bank provider instance with family-specific credentials
  def self.build_provider(family:)
    return nil unless family.present?

    # Get family-specific credentials
    my_bank_item = family.my_bank_items.where.not(api_key: nil).first
    return nil unless my_bank_item&.credentials_configured?

    # TODO: Implement provider initialization
    Provider::MyBank.new(
      my_bank_item.api_key,
      base_url: my_bank_item.effective_base_url,
      refresh_token: my_bank_item.refresh_token
    )
  end

  def sync_path
    Rails.application.routes.url_helpers.sync_my_bank_item_path(item)
  end

  def item
    provider_account.my_bank_item
  end

  def can_delete_holdings?
    false
  end

  def institution_domain
    metadata = provider_account.institution_metadata
    return nil unless metadata.present?
    metadata["domain"]
  end

  def institution_name
    metadata = provider_account.institution_metadata
    return nil unless metadata.present?
    metadata["name"] || item&.institution_name
  end

  def institution_url
    metadata = provider_account.institution_metadata
    return nil unless metadata.present?
    metadata["url"] || item&.institution_url
  end

  def institution_color
    item&.institution_color
  end
end
```

### 4. Panel View

**File:** `app/views/settings/providers/_my_bank_panel.html.erb`

A simple manual form for configuring My Bank credentials:

```erb
<div class="space-y-4">
  <div class="prose prose-sm text-secondary">
    <p class="text-primary font-medium">Setup instructions:</p>
    <ol>
      <li>Visit your My Bank dashboard to get your credentials</li>
      <li>Enter your credentials below and click the Save button</li>
      <li>After a successful connection, go to the Accounts tab to set up new accounts</li>
    </ol>

    <p class="text-primary font-medium">Field descriptions:</p>
    <ul>
      <li><strong>API Key:</strong> Your My Bank API key (required)</li>
      <li><strong>Base URL:</strong> Your My Bank base URL (optional, defaults to https://api.mybank.com)</li>
      <li><strong>Refresh Token:</strong> Your My Bank refresh token (required)</li>
    </ul>
  </div>

  <% error_msg = local_assigns[:error_message] || @error_message %>
  <% if error_msg.present? %>
    <div class="p-2 rounded-md bg-destructive/10 text-destructive text-sm">
      <%= error_msg %>
    </div>
  <% end %>

  <%
    # Get or initialize a my_bank_item for this family
    my_bank_item = Current.family.my_bank_items.first_or_initialize(name: "My Bank Connection")
    is_new_record = my_bank_item.new_record?
  %>

  <%= styled_form_with model: my_bank_item,
                       url: is_new_record ? my_bank_items_path : my_bank_item_path(my_bank_item),
                       scope: :my_bank_item,
                       method: is_new_record ? :post : :patch,
                       data: { turbo: true },
                       class: "space-y-3" do |form| %>
    <%= form.text_field :api_key,
                        label: "API Key",
                        type: :password %>

    <%= form.text_field :base_url,
                        label: "Base URL (Optional)",
                        placeholder: "https://api.mybank.com (default)",
                        value: my_bank_item.base_url %>

    <%= form.text_field :refresh_token,
                        label: "Refresh Token",
                        type: :password %>

    <div class="flex justify-end">
      <%= form.submit is_new_record ? "Save Configuration" : "Update Configuration",
                      class: "inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium text-white bg-gray-900 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2 transition-colors" %>
    </div>
  <% end %>

  <% items = local_assigns[:my_bank_items] || @my_bank_items || Current.family.my_bank_items.where.not(api_key: nil) %>
  <% if items&.any? %>
    <div class="flex items-center gap-2">
      <div class="w-2 h-2 bg-success rounded-full"></div>
      <p class="text-sm text-secondary">Configured and ready to use. Visit the <a href="<%= accounts_path %>" class="link">Accounts</a> tab to manage and set up accounts.</p>
    </div>
  <% end %>
</div>
```

### 5. Controller

**File:** `app/controllers/my_bank_items_controller.rb`

A simple controller with CRUD actions and Turbo Stream support:

```ruby
class MyBankItemsController < ApplicationController
  before_action :set_my_bank_item, only: [:update, :destroy, :sync]

  def create
    @my_bank_item = Current.family.my_bank_items.build(my_bank_item_params)
    @my_bank_item.name ||= "My Bank Connection"

    if @my_bank_item.save
      if turbo_frame_request?
        flash.now[:notice] = t(".success", default: "Successfully configured My Bank.")
        @my_bank_items = Current.family.my_bank_items.ordered
        render turbo_stream: [
          turbo_stream.replace(
            "my-bank-providers-panel",
            partial: "settings/providers/my_bank_panel",
            locals: { my_bank_items: @my_bank_items }
          ),
          *flash_notification_stream_items
        ]
      else
        redirect_to settings_providers_path, notice: t(".success"), status: :see_other
      end
    else
      @error_message = @my_bank_item.errors.full_messages.join(", ")

      if turbo_frame_request?
        render turbo_stream: turbo_stream.replace(
          "my-bank-providers-panel",
          partial: "settings/providers/my_bank_panel",
          locals: { error_message: @error_message }
        ), status: :unprocessable_entity
      else
        redirect_to settings_providers_path, alert: @error_message, status: :unprocessable_entity
      end
    end
  end

  def update
    if @my_bank_item.update(my_bank_item_params)
      if turbo_frame_request?
        flash.now[:notice] = t(".success", default: "Successfully updated My Bank configuration.")
        @my_bank_items = Current.family.my_bank_items.ordered
        render turbo_stream: [
          turbo_stream.replace(
            "my-bank-providers-panel",
            partial: "settings/providers/my_bank_panel",
            locals: { my_bank_items: @my_bank_items }
          ),
          *flash_notification_stream_items
        ]
      else
        redirect_to settings_providers_path, notice: t(".success"), status: :see_other
      end
    else
      @error_message = @my_bank_item.errors.full_messages.join(", ")

      if turbo_frame_request?
        render turbo_stream: turbo_stream.replace(
          "my-bank-providers-panel",
          partial: "settings/providers/my_bank_panel",
          locals: { error_message: @error_message }
        ), status: :unprocessable_entity
      else
        redirect_to settings_providers_path, alert: @error_message, status: :unprocessable_entity
      end
    end
  end

  def destroy
    @my_bank_item.destroy_later
    redirect_to settings_providers_path, notice: t(".success", default: "Scheduled My Bank connection for deletion.")
  end

  def sync
    unless @my_bank_item.syncing?
      @my_bank_item.sync_later
    end

    respond_to do |format|
      format.html { redirect_back_or_to accounts_path }
      format.json { head :ok }
    end
  end

  private

  def set_my_bank_item
    @my_bank_item = Current.family.my_bank_items.find(params[:id])
  end

  def my_bank_item_params
    params.require(:my_bank_item).permit(
      :name,
      :sync_start_date,
      :api_key,
      :base_url,
      :refresh_token
    )
  end
end
```

### 6. Routes

**File:** `config/routes.rb` (updated)

```ruby
resources :my_bank_items, only: [:create, :update, :destroy] do
  member do
    post :sync
  end
end
```

### 7. Settings Updates

**File:** `app/controllers/settings/providers_controller.rb` (updated)
- Excludes `my_bank` from global provider configurations
- Adds `@my_bank_items` instance variable

**File:** `app/views/settings/providers/show.html.erb` (updated)
- Adds My Bank section with turbo frame

---

## Customization

After generation, you'll typically want to customize three files:

### 1. Customize the Adapter

Implement the `build_provider` method in `app/models/provider/my_bank_adapter.rb`:

```ruby
def self.build_provider(family:)
  return nil unless family.present?

  # Get the family's credentials
  my_bank_item = family.my_bank_items.where.not(api_key: nil).first
  return nil unless my_bank_item&.credentials_configured?

  # Initialize your provider SDK with the credentials
  Provider::MyBank.new(
    my_bank_item.api_key,
    base_url: my_bank_item.effective_base_url,
    refresh_token: my_bank_item.refresh_token
  )
end
```

### 2. Update the Model

Add custom validations, helper methods, and business logic in `app/models/my_bank_item.rb`:

```ruby
class MyBankItem < ApplicationRecord
  include Syncable, Provided

  belongs_to :family

  # Validations (the generator adds basic ones, customize as needed)
  validates :name, presence: true
  validates :api_key, presence: true, on: :create
  validates :refresh_token, presence: true, on: :create
  validates :base_url, format: { with: URI::DEFAULT_PARSER.make_regexp }, allow_blank: true

  # Add custom business logic
  def refresh_oauth_token!
    # Implement OAuth token refresh logic
    provider = my_bank_provider
    return false unless provider

    new_token = provider.refresh_token!(refresh_token)
    update(refresh_token: new_token)
  rescue Provider::MyBank::AuthenticationError
    update(status: :requires_update)
    false
  end

  # Override effective methods for better defaults
  def effective_base_url
    base_url.presence || "https://api.mybank.com"
  end
end
```

### 3. Customize the View

Edit the generated panel view `app/views/settings/providers/_my_bank_panel.html.erb` to add custom content:

```erb
<div class="space-y-4">
  <%# Add custom header %>
  <div class="flex items-center gap-3">
    <%= image_tag "my_bank_logo.svg", class: "w-8 h-8" %>
    <h3 class="text-lg font-semibold">My Bank Integration</h3>
  </div>

  <%# The generated form content goes here... %>

  <%# Add custom help section %>
  <div class="mt-4 p-4 bg-subtle rounded-lg">
    <p class="text-sm text-secondary">
      Need help? Visit <a href="https://help.mybank.com" class="link">My Bank Help Center</a>
    </p>
  </div>
</div>
```

---

## Examples

### Example 1: Simple API Key Provider

```bash
rails g provider:family coinbase api_key:text:secret
```

Result: Basic provider with just an API key field.

### Example 2: OAuth Provider

```bash
rails g provider:family stripe \
  client_id:string:secret \
  client_secret:string:secret \
  access_token:text:secret \
  refresh_token:text:secret
```

Then customize the adapter to implement OAuth flow.

### Example 3: Complex Provider

```bash
rails g provider:family enterprise_bank \
  api_key:text:secret \
  environment:string \
  base_url:string \
  webhook_secret:text:secret \
  rate_limit:integer
```

Then add custom validations and logic in the model:

```ruby
class EnterpriseBankItem < ApplicationRecord
  # ... (basic setup)

  validates :environment, inclusion: { in: %w[sandbox production] }
  validates :rate_limit, numericality: { greater_than: 0 }, allow_nil: true

  def effective_rate_limit
    rate_limit || 100  # Default to 100 requests/minute
  end
end
```

---

## Tips & Best Practices

### 1. Always Run Migrations

```bash
rails db:migrate
```

### 2. Test in Console

```ruby
# Check if adapter is registered
Provider::Factory.adapters
# => { ... "MyBankAccount" => Provider::MyBankAdapter, ... }

# Test provider building
family = Family.first
item = family.my_bank_items.create!(name: "Test", api_key: "test_key", refresh_token: "test_refresh")
provider = Provider::MyBankAdapter.build_provider(family: family)
```

### 3. Use Proper Encryption

Always check that encryption is set up:

```ruby
# In your model
if Rails.application.credentials.active_record_encryption.present?
  encrypts :api_key, :refresh_token, deterministic: true
else
  Rails.logger.warn "ActiveRecord encryption not configured for #{self.name}"
end
```

### 4. Implement Proper Error Handling

```ruby
def self.build_provider(family:)
  return nil unless family.present?

  item = family.my_bank_items.where.not(api_key: nil).first
  return nil unless item&.credentials_configured?

  begin
    Provider::MyBank.new(item.api_key)
  rescue Provider::MyBank::ConfigurationError => e
    Rails.logger.error("MyBank provider configuration error: #{e.message}")
    nil
  end
end
```

### 5. Add Integration Tests

```ruby
# test/models/provider/my_bank_adapter_test.rb
class Provider::MyBankAdapterTest < ActiveSupport::TestCase
  test "builds provider with valid credentials" do
    family = families(:family_one)
    item = family.my_bank_items.create!(
      name: "Test Bank",
      api_key: "test_key"
    )

    provider = Provider::MyBankAdapter.build_provider(family: family)
    assert_not_nil provider
    assert_instance_of Provider::MyBank, provider
  end

  test "returns nil without credentials" do
    family = families(:family_one)
    provider = Provider::MyBankAdapter.build_provider(family: family)
    assert_nil provider
  end
end
```

---

## Troubleshooting

### Panel Not Showing

1. Check that the provider is excluded in `settings/providers_controller.rb`:
   ```ruby
   @provider_configurations = Provider::ConfigurationRegistry.all.reject do |config|
     config.provider_key.to_s.casecmp("my_bank").zero?
   end
   ```

2. Check that the instance variable is set:
   ```ruby
   @my_bank_items = Current.family.my_bank_items.ordered.select(:id)
   ```

3. Check that the section exists in `settings/providers/show.html.erb`:
   ```erb
   <%= settings_section title: "My Bank" do %>
     <turbo-frame id="my-bank-providers-panel">
       <%= render "settings/providers/my_bank_panel" %>
     </turbo-frame>
   <% end %>
   ```

### Form Not Submitting

1. Check routes are properly added:
   ```bash
   rails routes | grep my_bank
   ```

2. Check turbo frame ID matches:
   - View: `<turbo-frame id="my-bank-providers-panel">`
   - Controller: Uses `"my-bank-providers-panel"` in turbo_stream.replace

### Encryption Not Working

1. Check credentials are configured:
   ```bash
   rails credentials:edit
   ```

2. Add encryption keys if missing:
   ```yaml
   active_record_encryption:
     primary_key: (generate with: rails db:encryption:init)
     deterministic_key: (generate with: rails db:encryption:init)
     key_derivation_salt: (generate with: rails db:encryption:init)
   ```

3. Or use environment variables:
   ```bash
   export ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="..."
   export ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="..."
   export ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="..."
   ```

---

## Advanced: Creating a Provider SDK

For complex providers, consider creating a separate SDK class:

```ruby
# app/models/provider/my_bank.rb
class Provider::MyBank
  class AuthenticationError < StandardError; end
  class RateLimitError < StandardError; end

  def initialize(api_key, base_url: "https://api.mybank.com")
    @api_key = api_key
    @base_url = base_url
    @client = HTTP.headers(
      "Authorization" => "Bearer #{api_key}",
      "User-Agent" => "MyApp/1.0"
    )
  end

  def get_accounts
    response = @client.get("#{@base_url}/accounts")
    handle_response(response)
  end

  def get_transactions(account_id, start_date: nil, end_date: nil)
    params = { account_id: account_id }
    params[:start_date] = start_date.iso8601 if start_date
    params[:end_date] = end_date.iso8601 if end_date

    response = @client.get("#{@base_url}/transactions", params: params)
    handle_response(response)
  end

  private

  def handle_response(response)
    case response.code
    when 200...300
      JSON.parse(response.body, symbolize_names: true)
    when 401, 403
      raise AuthenticationError, "Invalid API key"
    when 429
      raise RateLimitError, "Rate limit exceeded"
    else
      raise StandardError, "API error: #{response.code} #{response.body}"
    end
  end
end
```

---

## Summary

The per-family `Provider` Rails generator system provides:
  - ✅ **Fast development** - Generate in seconds, not hour
  - ✅ **Consistency** - All providers follow the same pattern
  - ✅ **Maintainability** - Clear structure and conventions
  - ✅ **Flexibility** - Easy to customize for complex needs
  - ✅ **Security** - Built-in encryption for sensitive fields
  - ✅ **Documentation** - Self-documenting with descriptions

Use it whenever you need to add a new provider where each family needs their own credentials.
