Product Order Receipt Emails
This feature automatically sends a confirmation email when a customer completes a product purchase (e.g., magazines, books) via Stripe.
How It Works
- Stripe Webhook: When a payment succeeds (
payment_intent.succeeded), Stripe sends a webhook - Filter: The system only processes payment intents with a
code_tarifmetadata field prefixed withprice_ - Async Job: A
Premium::SendProductOrderReceiptJobis enqueued - Email Delivery: The job retrieves data from Stripe and sends the email via SendGrid
Technical Flow
Stripe Webhook (payment_intent.succeeded)
↓
config/initializers/stripe_event.rb
↓ (filters by metadata['code_tarif'].start_with?('price_'))
Premium::SendProductOrderReceiptJob
↓
Premium::SendgridService#send_order_receipt_email
↓
SendGrid Template: PREMIUM-ORDER-{LOCALE}
SendGrid Template
The template must be named using the format PREMIUM-ORDER-{LOCALE}, for example:
PREMIUM-ORDER-FRfor FrenchPREMIUM-ORDER-ENfor English
Dynamic Data for SendGrid
The job sends the following dynamic data to the SendGrid template:
{
"customer_email": "[email protected]",
"amount": 1500,
"currency": "eur",
"formatted_amount": "€15.00",
"product": {
"id": "prod_TWwvpMJYQLL7rZ",
"name": "Aleteia Magazine",
"description": "This is a truly awesome magazine by Aleteia",
"images": [],
"metadata": {
"edition": "2025",
"product_type": "magazine"
}
},
"price": {
"id": "price_1SZt1HJ2tNxPD0yXIzEwvsVS",
"unit_amount": 1500,
"currency": "eur",
"nickname": "French Reader price",
"metadata": {
"digital": "false"
}
},
"payment_intent_id": "pi_xxx",
"metadata": {
"code_tarif": "price_xxx",
"..."
}
}
Template Fields Reference
| Field | Description | Example |
|---|---|---|
formatted_amount | Formatted amount with currency symbol | €15.00 |
product.name | Product name (from Stripe) | Aleteia Magazine |
product.description | Product description (from Stripe) | This is a truly awesome... |
product.metadata.* | Custom product metadata | (see below) |
price.nickname | Price nickname | French Reader price |
price.metadata.* | Custom price metadata | (see below) |
Stripe Dashboard Configuration
To ensure receipt emails display correctly, Products and Prices must be properly configured in Stripe.
Required Fields (Built-in Stripe Fields)
These fields should always be filled in for a good email result:
Product Configuration
In Stripe Dashboard > Products:
| Field | Example | Notes |
|---|---|---|
| Name | Aleteia Magazine | Required - Displayed in the email |
| Description | This is a truly awesome magazine by Aleteia | Recommended - Displayed in the email |
Price Configuration
| Field | Example | Notes |
|---|---|---|
| Nickname | French Reader price | Recommended - Helps identify the price |
Optional Metadata (Custom Fields)
Metadata fields are optional and allow you to pass additional custom information to your email templates. The examples below demonstrate how you can use metadata to enrich your emails, but they are not required.
Example Product Metadata
| Key | Example Value | Purpose |
|---|---|---|
product_type | magazine | Categorize products (e.g., for conditional template logic) |
edition | 2025 | Display edition/version information |
Example Price Metadata
| Key | Example Value | Purpose |
|---|---|---|
digital | false | Indicate if the product is digital or physical |
You can add any custom metadata keys that make sense for your use case. All metadata is passed to the SendGrid template under product.metadata and price.metadata, giving you full flexibility in your email design.
Staging Example
Product (prod_TWwvpMJYQLL7rZ):
- Name:
Aleteia Magazine - Description:
This is a truly awesome magazine by Aleteia - Metadata (optional examples):
edition:2025product_type:magazine
Price (price_1SZt1HJ2tNxPD0yXIzEwvsVS):
- Unit Amount:
1500(€15.00) - Currency:
eur - Nickname:
French Reader price - Metadata (optional examples):
digital:false
Missing Customer Handling
Stripe may create the customer asynchronously after payment_intent.succeeded. The job handles this race condition with:
- Exponential backoff retry: 5s, 25s, 125s, 625s
- Timeout: If the customer is still not attached after 5 minutes, the job fails with
CustomerAttachmentTimeoutError
Notes
- The locale is currently hardcoded to
fr, can be extended in the future - Amount is formatted using the
Moneygem for proper currency display - All custom metadata from product/price is passed to the template for maximum flexibility