Skip to main content

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

  1. Stripe Webhook: When a payment succeeds (payment_intent.succeeded), Stripe sends a webhook
  2. Filter: The system only processes payment intents with a code_tarif metadata field prefixed with price_
  3. Async Job: A Premium::SendProductOrderReceiptJob is enqueued
  4. 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-FR for French
  • PREMIUM-ORDER-EN for 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

FieldDescriptionExample
formatted_amountFormatted amount with currency symbol€15.00
product.nameProduct name (from Stripe)Aleteia Magazine
product.descriptionProduct description (from Stripe)This is a truly awesome...
product.metadata.*Custom product metadata(see below)
price.nicknamePrice nicknameFrench 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:

FieldExampleNotes
NameAleteia MagazineRequired - Displayed in the email
DescriptionThis is a truly awesome magazine by AleteiaRecommended - Displayed in the email

Price Configuration

FieldExampleNotes
NicknameFrench Reader priceRecommended - 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

KeyExample ValuePurpose
product_typemagazineCategorize products (e.g., for conditional template logic)
edition2025Display edition/version information

Example Price Metadata

KeyExample ValuePurpose
digitalfalseIndicate 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: 2025
    • product_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 Money gem for proper currency display
  • All custom metadata from product/price is passed to the template for maximum flexibility