README.md

# PaddleBilling

Elixir library for Paddle Billing API v2.

Dual-write CRUD, two-way sync with drift detection, webhook verification, checkout flow, and installable admin panel.

## Features

- **Full API client** - Products, Prices, Discounts, Transactions with pagination and rate limit handling
- **Dual-write CRUD** - create/update in Paddle API + local database atomically
- **Two-way sync** - pull from Paddle, push to Paddle, or reconcile with strategy (`:paddle_wins`, `:local_wins`, `:newest_wins`)
- **Drift detection** - SHA256 checksums to detect data divergence
- **Webhook verification** - HMAC-SHA256 signature + replay protection
- **Auto-sync** - webhooks + periodic Oban reconciliation
- **Checkout flow** - Transaction creation + Paddle.js LiveView hook
- **Admin panel** - installable LiveViews for managing products, prices, discounts, and sync

## Requirements

- Elixir 1.15+
- Phoenix LiveView 0.20+
- Req (HTTP client)
- Oban (background jobs)
- PostgreSQL

## Installation

### 1. Add as a dependency

Add `paddle_billing` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:paddle_billing, github: "safemyprivacy0-bit/paddle_billing"}
  ]
end
```

### 2. Configure environment variables

```bash
export PADDLE_BILLING_ENVIRONMENT=sandbox    # or "production"
export PADDLE_BILLING_API_KEY=pdl_sdbx_apikey_xxx
export PADDLE_BILLING_CLIENT_TOKEN=test_xxx  # for Paddle.js checkout
export PADDLE_BILLING_SIGNING_SECRET=pdl_ntfset_xxx
```

### 3. Add config to `config/runtime.exs`

```elixir
config :paddle_billing, :config,
  environment: System.get_env("PADDLE_BILLING_ENVIRONMENT", "sandbox"),
  api_key: System.get_env("PADDLE_BILLING_API_KEY"),
  client_token: System.get_env("PADDLE_BILLING_CLIENT_TOKEN"),
  signing_secret: System.get_env("PADDLE_BILLING_SIGNING_SECRET")
```

### 4. Create Ecto schemas and migration

Copy the integration layer to your project:

```
lib/your_app/billing.ex              # Context (public API)
lib/your_app/billing/paddle_product.ex
lib/your_app/billing/paddle_price.ex
lib/your_app/billing/paddle_discount.ex
lib/your_app/billing/sync.ex
```

Copy and run the migration:

```bash
mix ecto.migrate
```

### 5. Set up webhooks

Copy the webhook controller and plug:

```
lib/your_app_web/controllers/paddle_webhook_controller.ex
lib/your_app_web/plugs/paddle_webhook_signature.ex
```

Add the webhook route to your `router.ex`:

```elixir
scope "/webhooks", YourAppWeb do
  pipe_through :paddle_webhook
  post "/paddle", PaddleWebhookController, :handle
end
```

Add the raw body caching to `endpoint.ex` for the webhook path.

### 6. Install admin panel (optional)

```bash
mix paddle_billing.install
```

Options:

```bash
mix paddle_billing.install --web-module MyAppWeb --context-module MyApp.Billing
mix paddle_billing.install --no-routes  # skip route injection, print instructions
```

The installer:
- Copies LiveViews to `lib/your_app_web/live/paddle/`
- Copies components to `lib/your_app_web/components/paddle_components.ex`
- Copies Paddle.js hook to `assets/js/hooks/paddle_checkout.js`
- Injects routes into `router.ex`

After installing, register the JS hook in `assets/js/hooks/index.js`:

```javascript
import PaddleCheckout from "./paddle_checkout"

export default {
  // ...existing hooks
  PaddleCheckout,
}
```

### 7. Set up auto-sync (optional)

Copy the Oban worker:

```
lib/your_app/workers/paddle_sync_worker.ex
```

Add to Oban crontab in `config/config.exs`:

```elixir
config :your_app, Oban,
  queues: [default: 10],
  plugins: [
    {Oban.Plugins.Cron,
     crontab: [
       {"0 */6 * * *", YourApp.Workers.PaddleSyncWorker}
     ]}
  ]
```

## Usage

### API Client (direct)

```elixir
# List products from Paddle API
{:ok, products} = PaddleBilling.Products.list_all()

# Create a product
{:ok, product} = PaddleBilling.Products.create(%{
  "name" => "Pro Plan",
  "tax_category" => "standard"
})

# Get a price
{:ok, price} = PaddleBilling.Prices.get("pri_01h...")
```

### Billing Context (dual-write)

All operations write to Paddle API and local database atomically:

```elixir
alias YourApp.Billing

# Create product (Paddle + local DB)
{:ok, product} = Billing.create_product(%{
  "name" => "Pro Plan",
  "tax_category" => "standard",
  "plan_level" => "starter",
  "app_role" => "subscription"
})

# Create price for product
{:ok, price} = Billing.create_price(product, %{
  "amount" => 2900,           # in cents ($29.00)
  "currency_code" => "USD",
  "billing_cycle_interval" => "month",
  "billing_cycle_frequency" => 1,
  "description" => "Monthly Pro"
})

# Update product
{:ok, updated} = Billing.update_product(product, %{"name" => "Pro Plan v2"})

# Archive
{:ok, archived} = Billing.archive_product(product)

# List from local DB (fast, no API call)
products = Billing.list_products(status: "active", app_role: :subscription)
prices = Billing.list_prices_for_product(product.paddle_id)
discounts = Billing.list_discounts(status: "active")
```

### Sync

```elixir
# Pull everything from Paddle -> local DB
{:ok, results} = Billing.sync_all_from_paddle()

# Pull one resource type
{:ok, products} = Billing.sync_from_paddle(:products)
{:ok, prices} = Billing.sync_from_paddle(:prices)
{:ok, discounts} = Billing.sync_from_paddle(:discounts)

# Detect drift (compare local checksums vs Paddle API)
{:ok, drift} = Billing.detect_drift(:products)
# => [{%PaddleProduct{}, :in_sync}, {%PaddleProduct{}, :drifted}, ...]

# Reconcile with strategy
{:ok, results} = Billing.reconcile(:products, strategy: :paddle_wins)   # Paddle overwrites local
{:ok, results} = Billing.reconcile(:products, strategy: :local_wins)    # Local pushes to Paddle
{:ok, results} = Billing.reconcile(:products, strategy: :newest_wins)   # Newer timestamp wins
```

### Checkout

```elixir
# Create a checkout session (Paddle Transaction)
{:ok, transaction_id} = Billing.create_checkout(
  ["pri_01h..."],                          # price IDs
  custom_data: %{"account_id" => 123}      # passed to webhooks
)

# Preview pricing without creating a transaction
{:ok, preview} = Billing.preview_checkout(
  ["pri_01h..."],
  currency_code: "EUR",
  discount_id: "dsc_01h..."
)

# Get params for Paddle.js frontend
params = Billing.checkout_params(["pri_01h..."],
  success_url: "https://example.com/success",
  display_mode: "overlay"
)

# Get client token and environment for frontend
Billing.client_token()   # => "test_xxx"
Billing.environment()    # => :sandbox
```

Frontend checkout route: `/checkout?price_id=pri_01h...` or `/checkout?price_ids=pri_01h...,pri_02h...`

### Webhook verification

```elixir
# Verify webhook signature manually
:ok = PaddleBilling.Webhook.Verifier.verify(
  raw_body,
  paddle_signature_header,
  signing_secret,
  max_age: 300
)
```

The webhook controller handles this automatically via the `PaddleWebhookSignature` plug.

## Admin Panel

After running `mix paddle_billing.install`, the following routes are available:

| Route | Description |
|-------|-------------|
| `/admin/billing` | Products list |
| `/admin/billing/products/new` | Create product |
| `/admin/billing/products/:id/edit` | Edit product |
| `/admin/billing/products/:id/prices` | Prices for product |
| `/admin/billing/products/:id/prices/new` | Create price |
| `/admin/billing/discounts` | Discounts list |
| `/admin/billing/discounts/new` | Create discount |
| `/admin/billing/sync` | Sync dashboard (drift detection, reconciliation) |
| `/checkout` | Paddle.js checkout (authenticated users) |

Admin routes require the `:require_admin` LiveAuth on_mount hook.

## Library Structure

```
lib/
  paddle_billing.ex              # Public facade with delegates
  paddle_billing/
    config.ex                    # Env-based configuration
    client.ex                    # Req HTTP client + pagination + rate limits
    error.ex                     # Error structs
    resources/
      product.ex                 # Products CRUD
      price.ex                   # Prices CRUD
      discount.ex                # Discounts CRUD
      transaction.ex             # Transactions CRUD + preview
    webhook/
      verifier.ex                # HMAC-SHA256 + replay protection

priv/templates/                  # Installable admin panel templates
  components/
    paddle_components.ex         # Shared function components
  live/paddle/
    products_live.ex             # Products management
    prices_live.ex               # Prices management
    discounts_live.ex            # Discounts management
    sync_live.ex                 # Sync dashboard
    checkout_live.ex             # Paddle.js checkout
  js/
    paddle_checkout.js           # LiveView JS hook for Paddle.js

lib/mix/tasks/
  paddle_billing.install.ex      # Mix task installer
```

## Paddle API v2 Reference

- Base URL (sandbox): `https://sandbox-api.paddle.com`
- Base URL (production): `https://api.paddle.com`
- Auth: `Authorization: Bearer {api_key}`
- Pagination: cursor-based (`meta.pagination.next`, `meta.pagination.has_more`)
- Amounts: strings in smallest unit (e.g. `"2900"` = $29.00)
- Webhook signature: `Paddle-Signature: ts=TIMESTAMP;h1=HMAC_SHA256`