Current section

Files

Jump to
pin_stripe usage-rules.md
Raw

usage-rules.md

# PinStripe Usage Rules
A minimal Stripe SDK for Elixir with webhook handling, built on Req and Spark.
## Installation
### Using Igniter (Recommended)
Install using the Igniter installer, which handles everything automatically:
```bash
mix igniter.install pin_stripe
```
This automatically:
- Adds the dependency to your `mix.exs`
- Configures your Phoenix endpoint for webhook signature verification
- Creates webhook handler and controller modules
- Sets up routing
- Configures code formatting
### Manual Installation
If not using Igniter, add to your `mix.exs`:
```elixir
{:pin_stripe, "~> 0.1"}
```
Then manually configure endpoints, handlers, and routes (see Manual Setup section in README).
## Configuration
Configure your Stripe credentials. Typically done in `config/runtime.exs`:
```elixir
config :pin_stripe,
stripe_api_key: System.get_env("YOUR_STRIPE_KEY_ENV_VAR"),
stripe_webhook_secret: System.get_env("YOUR_WEBHOOK_SECRET_ENV_VAR")
```
**Required configuration keys:**
- `:pin_stripe, :stripe_api_key` - Your Stripe API key for making requests
- `:pin_stripe, :stripe_webhook_secret` - Your webhook signing secret for verifying webhooks
**Important**: Never commit API keys to version control. Always use environment variables or a secrets manager.
## Making API Requests
Use `PinStripe.request/2` to make Stripe API calls:
```elixir
# GET request
{:ok, customer} = PinStripe.request(:get, "/v1/customers/cus_123")
# POST request with params
{:ok, customer} = PinStripe.request(:post, "/v1/customers",
email: "customer@example.com",
name: "Jane Doe"
)
# DELETE request
{:ok, _} = PinStripe.request(:delete, "/v1/customers/cus_123")
```
All requests return `{:ok, response}` or `{:error, reason}` tuples.
## Webhook Handling
### WebhookHandler Module
The installer creates a `StripeWebhookHandlers` module. Define handlers using the DSL:
```elixir
defmodule MyApp.StripeWebhookHandlers do
use PinStripe.WebhookHandler
# Function handler - inline
handle "customer.created", fn event ->
customer = event.data.object
# Handle the event
:ok
end
# Module handler - separate module
handle "invoice.paid", MyApp.InvoicePaidHandler
end
```
**Important**:
- Always return `:ok` from handlers to acknowledge successful processing
- Return `{:error, reason}` to indicate processing failure (webhook will be retried by Stripe)
- The `event` parameter contains the full Stripe event object
### Handler Types
**Function Handlers** - Quick inline handlers:
```elixir
handle "customer.updated", fn event ->
# Process event inline
:ok
end
```
**Module Handlers** - Better for complex logic:
```elixir
# In your WebhookHandler module
handle "subscription.created", MyApp.SubscriptionCreatedHandler
# Separate module
defmodule MyApp.SubscriptionCreatedHandler do
def handle_event(event) do
subscription = event.data.object
# Complex processing logic
:ok
end
end
```
### Generating Handlers
Use the generator to create handlers quickly:
```bash
# Generate a function handler
mix pin_stripe.gen.handler customer.subscription.updated
# Generate a module handler
mix pin_stripe.gen.handler invoice.paid --handler-type module
```
### Webhook Controller
The installer creates `lib/my_app_web/stripe_webhook_controller.ex` which:
- Verifies webhook signatures automatically
- Routes events to your handlers
- Handles errors gracefully
**Note**: The controller is created in `lib/my_app_web/`, not in `lib/my_app_web/controllers/`. You can move it to the controllers directory if preferred.
### Security
The installer configures `PinStripe.ParsersWithRawBody` in your endpoint, which:
- Caches the raw request body for signature verification
- Is required for Stripe webhook security
- Replaces the standard `Plug.Parsers`
**Critical**: Never skip webhook signature verification in production. The installer handles this automatically.
## Common Patterns
### Idempotent Webhook Processing
Stripe may send the same webhook multiple times. Make your handlers idempotent:
```elixir
handle "payment_intent.succeeded", fn event ->
payment_intent_id = event.data.object.id
# Check if already processed
case MyApp.Payments.get_by_stripe_id(payment_intent_id) do
nil ->
# First time, process it
MyApp.Payments.create_from_stripe(event.data.object)
:ok
_existing ->
# Already processed, skip
:ok
end
end
```
### Error Handling
Return errors to have Stripe retry:
```elixir
handle "invoice.payment_failed", fn event ->
case MyApp.Billing.handle_failed_payment(event.data.object) do
{:ok, _} -> :ok
{:error, :temporary_failure} -> {:error, "Database unavailable, retry later"}
{:error, _reason} -> :ok # Don't retry for permanent failures
end
end
```
### Async Processing
For long-running operations, enqueue a job:
```elixir
handle "customer.subscription.deleted", fn event ->
# Quick acknowledgment, process async
MyApp.Jobs.queue_subscription_cancellation(event.data.object.id)
:ok
end
```
## Event Types
Common Stripe events:
- `customer.created`, `customer.updated`, `customer.deleted`
- `payment_intent.succeeded`, `payment_intent.payment_failed`
- `invoice.paid`, `invoice.payment_failed`
- `customer.subscription.created`, `customer.subscription.updated`, `customer.subscription.deleted`
- `charge.succeeded`, `charge.failed`, `charge.refunded`
View all supported events:
```bash
cat deps/pin_stripe/priv/supported_stripe_events.txt
```
## Testing
PinStripe provides comprehensive test helpers in `PinStripe.Test.Mock` and `PinStripe.Test.Fixtures` for testing your Stripe integration without hitting the real API.
### Test Helpers Overview
**`PinStripe.Test.Mock`** - High-level mocking functions for common operations:
- `stub_read/2` - Mock reading/listing resources
- `stub_create/2` - Mock creating resources
- `stub_update/2` - Mock updating resources
- `stub_delete/1` - Mock deleting resources
- `stub_error/1` or `stub_error/2` - Mock error responses
- `stub_fixture/1` or `stub_fixture/2` - Mock using pre-built fixtures
**`PinStripe.Test.Fixtures`** - Load realistic Stripe response data (can use live API or cached fixtures)
### Basic Mock Usage
```elixir
defmodule MyAppTest do
use ExUnit.Case
alias PinStripe.Test.Mock
test "creates a customer" do
# Mock the create response
Mock.stub_create(:customers, %{
"id" => "cus_123",
"email" => "test@example.com"
})
# Your application code
{:ok, customer} = MyApp.create_customer("test@example.com")
assert customer["id"] == "cus_123"
end
test "reads a customer" do
Mock.stub_read("cus_123", %{
"id" => "cus_123",
"email" => "test@example.com"
})
{:ok, customer} = MyApp.get_customer("cus_123")
assert customer["email"] == "test@example.com"
end
test "lists customers" do
Mock.stub_read(:customers, [
%{"id" => "cus_1", "email" => "user1@example.com"},
%{"id" => "cus_2", "email" => "user2@example.com"}
])
{:ok, customers} = MyApp.list_customers()
assert length(customers) == 2
end
end
```
### Testing Error Handling
Use `stub_error/1` with predefined error atoms or `stub_error/2` for custom errors:
```elixir
test "handles not found errors" do
Mock.stub_error(:not_found)
assert {:error, %{status: 404}} = MyApp.get_customer("cus_invalid")
end
test "handles rate limiting" do
Mock.stub_error(:rate_limit)
assert {:error, %{status: 429}} = MyApp.create_customer("test@example.com")
end
test "handles custom validation errors" do
Mock.stub_error(:bad_request, %{
message: "Invalid email address",
param: "email"
})
assert {:error, response} = MyApp.create_customer("invalid")
assert response.body["error"]["param"] == "email"
end
```
**Available error atoms:**
- `:not_found` (404) - Resource doesn't exist
- `:bad_request` (400) - Missing or invalid parameters
- `:unauthorized` (401) - Invalid API key
- `:rate_limit` (429) - Too many requests
- `:server_error` (500) - Stripe server error
### Using Error Fixtures
For realistic error responses, use `stub_fixture/1` with error atoms:
```elixir
test "handles card declined errors" do
Mock.stub_fixture(:error_402)
{:error, response} = MyApp.charge_card(payment_method)
assert response.body["error"]["type"] == "card_error"
assert response.body["error"]["code"] == "card_declined"
end
test "handles idempotency conflicts" do
Mock.stub_fixture(:error_409)
{:error, response} = MyApp.create_payment(idempotency_key: "duplicate")
assert response.body["error"]["type"] == "idempotency_error"
end
```
**Available error fixtures:**
- `:error_400` - Bad Request (missing required parameter)
- `:error_401` - Unauthorized (invalid API key)
- `:error_402` - Request Failed (card declined)
- `:error_403` - Forbidden (insufficient permissions)
- `:error_404` - Not Found (resource doesn't exist)
- `:error_409` - Conflict (idempotency key in use)
- `:error_424` - External Dependency Failed
- `:error_429` - Too Many Requests (rate limit)
- `:error_500`, `:error_502`, `:error_503`, `:error_504` - Server Errors
### Testing with Real Stripe Data
Use fixtures to load real Stripe response data (requires Stripe CLI for first-time generation):
```elixir
test "handles real customer data structure" do
# Loads actual Stripe customer response (cached after first load)
Mock.stub_fixture("customer")
{:ok, customer} = MyApp.get_customer("cus_test")
# Test against real Stripe data structure
assert Map.has_key?(customer, "id")
assert Map.has_key?(customer, "email")
assert customer["object"] == "customer"
end
```
**Fixture Types:**
- **Error Fixtures** - Use atoms (`:error_404`, `:error_400`, etc.), self-contained, no Stripe CLI required
- **API Resources** - Use strings (`"customer"`, `"payment_intent"`), require Stripe CLI for initial generation
- **Webhook Events** - Use strings (`"customer.created"`), require Stripe CLI
Error fixtures are generated instantly and don't create cached files.
### Testing Webhooks Locally
Use the Stripe CLI to forward webhooks:
```bash
stripe listen --forward-to localhost:4000/webhooks/stripe
```
Trigger test events:
```bash
stripe trigger customer.created
stripe trigger payment_intent.succeeded
```
### Testing Webhook Handlers
Test webhook handlers directly without HTTP:
```elixir
test "handles customer.created event" do
event = %{
id: "evt_test",
type: "customer.created",
data: %{
object: %{
id: "cus_test",
email: "test@example.com"
}
}
}
assert :ok = MyApp.StripeWebhookHandlers.handle_event(event)
end
```
### Test Setup
In your `test_helper.exs`, configure the test adapter:
```elixir
# Use Req.Test adapter for mocking
Application.put_env(:pin_stripe, :req_options, plug: {Req.Test, PinStripe})
```
This allows `Mock` functions to intercept Stripe API calls in tests.
## Mix Tasks
- `mix pin_stripe.install` - Install and configure PinStripe
- `mix pin_stripe.gen.handler <event>` - Generate a handler for a specific event
- `mix pin_stripe.set_webhook_path <path>` - Update the webhook route path
- `mix pin_stripe.sync_webhook_handlers` - Sync handlers with Stripe (if using Spark introspection)
## Common Mistakes
- **Don't hardcode API keys**: Always use environment variables
- **Don't skip signature verification**: The installer configures this automatically
- **Don't block webhook handlers**: Keep handlers fast, enqueue long operations
- **Don't forget to return `:ok`**: Handlers must return `:ok` or `{:error, reason}`
- **Don't process webhooks twice**: Make handlers idempotent
- **Don't use in production without testing**: Test with Stripe CLI first
## Best Practices
1. **Keep handlers simple**: Complex logic should be in separate modules
2. **Log webhook processing**: Helpful for debugging
3. **Monitor webhook failures**: Set up alerts for repeated failures
4. **Version your API**: Stripe has multiple API versions, be consistent
5. **Handle all expected events**: Unhandled events are logged but don't cause errors
6. **Test with Stripe CLI**: Always test webhooks before deploying
## Troubleshooting
**Webhook signature verification fails**:
- Check that `ParsersWithRawBody` is configured in your endpoint
- Verify `:pin_stripe, :stripe_webhook_secret` config is set correctly
- Ensure you're using the secret from the Stripe webhook endpoint settings
**Events not being handled**:
- Check handler module is referenced in the WebhookController
- Verify handler is defined for that specific event type
- Check application logs for errors
**API requests failing**:
- Verify `:pin_stripe, :stripe_api_key` config is set
- Check API key has correct permissions
- Ensure you're using the correct API version