@spree/docs 0.1.181 → 0.1.183

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/api-reference/seller-api/errors.md +1 -1
  2. package/dist/api-reference/store.yaml +78 -91
  3. package/dist/developer/core-concepts/addresses.md +106 -198
  4. package/dist/developer/core-concepts/architecture.md +97 -126
  5. package/dist/developer/core-concepts/calculators.md +75 -252
  6. package/dist/developer/core-concepts/carts.md +1 -1
  7. package/dist/developer/core-concepts/channels.md +0 -4
  8. package/dist/developer/core-concepts/companies-and-catalogs.md +1 -1
  9. package/dist/developer/core-concepts/customers.md +0 -3
  10. package/dist/developer/core-concepts/discounts.md +133 -0
  11. package/dist/developer/core-concepts/events.md +83 -576
  12. package/dist/developer/core-concepts/fees.md +144 -0
  13. package/dist/developer/core-concepts/imports-exports.md +105 -679
  14. package/dist/developer/core-concepts/inventory.md +114 -248
  15. package/dist/developer/core-concepts/markets.md +9 -12
  16. package/dist/developer/core-concepts/media.md +9 -11
  17. package/dist/developer/core-concepts/metafields.md +123 -200
  18. package/dist/developer/core-concepts/order-totals.md +110 -0
  19. package/dist/developer/core-concepts/orders.md +1 -1
  20. package/dist/developer/core-concepts/payments.md +11 -14
  21. package/dist/developer/core-concepts/pricing.md +11 -13
  22. package/dist/developer/core-concepts/products.md +173 -19
  23. package/dist/developer/core-concepts/promotions.md +12 -11
  24. package/dist/developer/core-concepts/search-filtering.md +2 -4
  25. package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
  26. package/dist/developer/core-concepts/taxes.md +125 -113
  27. package/dist/developer/core-concepts/translations.md +61 -68
  28. package/dist/developer/core-concepts/webhooks.md +25 -59
  29. package/dist/developer/how-to/custom-promotion.md +3 -3
  30. package/package.json +1 -1
  31. package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
@@ -1,644 +1,151 @@
1
1
  ---
2
2
  title: Events
3
- description: Spree's event system publish and subscribe to lifecycle events from orders, products, and users to trigger background jobs and integrations.
3
+ description: Reacting to things that happen in Spree an order placed, a payment taken, stock running out without modifying core code.
4
4
  ---
5
5
 
6
- import { Since } from '/snippets/since.mdx';
7
-
8
-
9
6
  ## Overview
10
7
 
11
- Spree includes a powerful event system that allows you to react to various actions happening in your store. When something happens (an order is completed, a product is created, etc.), Spree publishes an event that your code can subscribe to and handle.
12
-
13
- This pattern enables loose coupling between components and makes it easy to:
14
-
15
- - Send email notifications when orders are placed
16
- - Sync data with external services when products change
17
- - Log audit trails for compliance
18
- - Trigger webhooks to notify third-party systems
19
- - Update caches when inventory changes
20
-
21
- ## How Events Work
22
-
23
- Spree's event system provides a clean API through:
8
+ Something happens in a store an order is placed, a payment clears, a product sells out and you want something else to happen: notify a warehouse, post to a channel, update a spreadsheet.
24
9
 
25
- 1. **`Spree::Events`** - The main module for publishing and subscribing to events
26
- 2. **`Spree::Subscriber`** - Base class for creating event subscribers
27
- 3. **`Spree::Publishable`** - Concern that enables models to publish events
28
-
29
- When an event is published, all matching subscribers are notified. By default, subscribers run asynchronously via background jobs to avoid blocking the main request.
10
+ Events are how you attach that behaviour without editing Spree. Spree announces what happened; your code decides what to do about it.
30
11
 
31
12
  ```mermaid
32
- flowchart TB
33
- subgraph Spree Application
34
- A[Model Action] --> B[publish_event]
35
- B --> C[Event Serializer]
36
- C --> D[Spree::Events]
37
- end
38
-
39
- subgraph Event Adapter
40
- D --> E[Find Matching Subscribers]
41
- E --> F{Async?}
42
- F -->|Yes| G[Queue Background Job]
43
- F -->|No| H[Execute Immediately]
44
- end
45
-
46
- subgraph Subscribers
47
- G --> I[SubscriberJob]
48
- I --> J[Your Subscriber]
49
- H --> J
50
- J --> K[Send Email]
51
- J --> L[Sync External Service]
52
- J --> M[Update Cache]
53
- J --> N[Trigger Webhook]
54
- end
55
- ```
56
-
57
- ## Creating a Subscriber
58
-
59
- The fastest path is the generator — it creates the class, a spec stub, and registers the subscriber in `config/initializers/spree.rb` (the step that's easy to forget):
60
-
61
-
62
- ```bash Spree CLI (Docker)
63
- spree generate subscriber OrderCompleted order.completed
64
- ```
65
-
66
- ```bash Without Spree CLI
67
- bin/rails g spree:subscriber OrderCompleted order.completed
68
- ```
69
-
70
-
71
- Or create the class by hand in `app/subscribers/`, inheriting from `Spree::Subscriber`:
72
-
73
- ```ruby app/subscribers/order_completed_subscriber.rb
74
- class OrderCompletedSubscriber < Spree::Subscriber
75
- subscribes_to 'order.completed'
76
-
77
- def handle(event)
78
- order_id = event.payload['id']
79
- order = Spree::Order.find_by_prefix_id(order_id)
80
- return unless order
81
-
82
- # Your custom logic here
83
- ExternalService.notify_order_placed(order)
84
- end
85
- end
13
+ flowchart LR
14
+ Action["Order is placed"] --> Event["order.placed"]
15
+ Event --> Sub["Your subscriber<br/>(in-app)"]
16
+ Event --> Hook["Webhook<br/>(external system)"]
86
17
  ```
87
18
 
88
- Then register it in an initializer subscribers are not auto-discovered (see [Registering Subscribers](#registering-subscribers)):
19
+ There are two ways to listen, and which you want depends on where your code lives:
89
20
 
90
- ```ruby config/initializers/spree.rb
91
- Rails.application.config.after_initialize do
92
- Spree.subscribers << OrderCompletedSubscriber
93
- end
94
- ```
95
-
96
- ### Subscriber DSL
97
-
98
- The `Spree::Subscriber` class provides a clean DSL for declaring subscriptions:
99
-
100
- ```ruby
101
- class MySubscriber < Spree::Subscriber
102
- # Subscribe to a single event
103
- subscribes_to 'order.completed'
104
-
105
- # Subscribe to multiple events
106
- subscribes_to 'order.completed', 'order.canceled', 'order.resumed'
107
-
108
- # Subscribe to all events matching a pattern
109
- subscribes_to 'order.*' # All order events
110
- subscribes_to '*.*' # All events (use sparingly!)
111
-
112
- # Run synchronously instead of via background job
113
- subscribes_to 'order.completed', async: false
114
- end
115
- ```
116
-
117
- ### Handling Multiple Events
118
-
119
- When subscribing to multiple events, use the `on` DSL to route events to specific methods:
120
-
121
- ```ruby app/subscribers/order_audit_subscriber.rb
122
- class OrderAuditSubscriber < Spree::Subscriber
123
- subscribes_to 'order.completed', 'order.canceled', 'order.resumed'
124
-
125
- on 'order.completed', :log_order_completed
126
- on 'order.canceled', :log_order_canceled
127
- on 'order.resumed', :log_order_resumed
128
-
129
- private
130
-
131
- def log_order_completed(event)
132
- create_audit_log(event, 'completed')
133
- end
134
-
135
- def log_order_canceled(event)
136
- create_audit_log(event, 'canceled')
137
- end
138
-
139
- def log_order_resumed(event)
140
- create_audit_log(event, 'resumed')
141
- end
142
-
143
- def create_audit_log(event, action)
144
- AuditLog.create!(
145
- resource_type: 'Spree::Order',
146
- resource_id: event.payload['id'],
147
- action: action,
148
- occurred_at: event.created_at
149
- )
150
- end
151
- end
152
- ```
153
-
154
- ## Working with Events
155
-
156
- ### Event Object
157
-
158
- When your subscriber receives an event, you get a `Spree::Event` object with:
159
-
160
- ```ruby
161
- def handle(event)
162
- event.id # => "550e8400-e29b-41d4-a716-446655440000" (UUID)
163
- event.name # => "order.completed"
164
- event.store_id # => 1 (ID of the store where the event originated)
165
- event.payload # => { "id" => 1, "number" => "R123456", ... }
166
- event.metadata # => { "spree_version" => "<spree_version>" }
167
- event.created_at # => Time when event was published
168
-
169
- # Helper methods
170
- event.store # => Spree::Store instance (lazy loaded)
171
- event.resource_type # => "order" (extracted from name)
172
- event.action # => "completed" (extracted from name)
173
- end
174
- ```
175
-
176
- ### Finding the Record
177
-
178
- The payload contains serialized attributes, not the actual record. To get the record:
179
-
180
- ```ruby
181
- def handle(event)
182
- record_id = event.payload['id']
183
- record = Spree::Order.find_by_prefix_id(record_id)
184
- return unless record
185
-
186
- # Work with the record
187
- end
188
- ```
189
-
190
- > **WARNING:** For destroy events, the record no longer exists in the database. Use the payload data instead, or capture what you need before deletion.
191
-
192
- ## Available Events
193
-
194
- ### Lifecycle Events
195
-
196
- Models that include `Spree::Publishable` and call `publishes_lifecycle_events` automatically publish:
197
-
198
- | Event Pattern | Description |
199
- |---------------|-------------|
200
- | `{model}.created` | Record was created |
201
- | `{model}.updated` | Record was updated |
202
- | `{model}.deleted` | Record was deleted |
203
-
204
- For example, `Spree::Price` publishes `price.created`, `price.updated`, and `price.deleted`.
205
-
206
- Models with lifecycle events enabled include: `Order`, `Payment`, `Price`, `Shipment`, `Variant`, `LineItem`, `StockItem`, and many others.
207
-
208
- ### Order Events
209
-
210
- | Event | Description |
211
- |-------|-------------|
212
- | `order.created` | Order was created |
213
- | `order.updated` | Order was updated |
214
- | `order.completed` | Order checkout completed |
215
- | `order.canceled` | Order was canceled |
216
- | `order.resumed` | Canceled order was resumed |
217
- | `order.paid` | Order is fully paid |
218
- | `order.shipped` | All order shipments are shipped |
219
-
220
- ### Shipment Events
221
-
222
- | Event | Description |
223
- |-------|-------------|
224
- | `shipment.created` | Shipment was created |
225
- | `shipment.updated` | Shipment was updated |
226
- | `shipment.shipped` | Shipment was shipped |
227
- | `shipment.canceled` | Shipment was canceled |
228
- | `shipment.resumed` | Shipment was resumed |
229
-
230
- ### Payment Events
231
-
232
- | Event | Description |
233
- |-------|-------------|
234
- | `payment.created` | Payment was created |
235
- | `payment.updated` | Payment was updated |
236
- | `payment.paid` | Payment was completed |
237
-
238
- ### Price Events
239
-
240
- | Event | Description |
241
- |-------|-------------|
242
- | `price.created` | Price was created |
243
- | `price.updated` | Price was updated |
244
- | `price.deleted` | Price was deleted |
245
-
246
- ### User Events
247
-
248
- | Event | Description |
249
- |-------|-------------|
250
- | `user.created` | User was created |
251
- | `user.updated` | User was updated |
252
- | `user.deleted` | User was deleted |
253
-
254
- When `Spree.admin_user_class` differs from `Spree.user_class`, admin users publish the equivalent `admin.*` events (see the Admin Events table below).
255
-
256
- ### Admin Events
257
-
258
- | Event | Description |
259
- |-------|-------------|
260
- | `admin.created` | Admin user was created |
261
- | `admin.updated` | Admin user was updated |
262
- | `admin.deleted` | Admin user was deleted |
263
-
264
- ### Product Events
265
-
266
- | Event | Description |
267
- |-------|-------------|
268
- | `product.activated` | Product status changed to active |
269
- | `product.archived` | Product status changed to archived |
270
- | `product.out_of_stock` | Product has no stock left for any variant |
271
- | `product.back_in_stock` | Product was out of stock and now has stock again |
272
-
273
- ## Publishing Custom Events
274
-
275
- You can publish custom events from anywhere in your application:
276
-
277
- ### From a Model
278
-
279
- Models including `Spree::Publishable` can use `publish_event`:
280
-
281
- ```ruby
282
- class Spree::Order < Spree.base_class
283
- def mark_as_fraudulent!
284
- update!(fraudulent: true)
285
- publish_event('order.marked_fraudulent')
286
- end
287
- end
288
- ```
21
+ | | Use it when |
22
+ |---|---|
23
+ | **[Webhooks](webhooks.md)** | The thing reacting is a separate system — a Next.js app, an ERP, an automation tool |
24
+ | **Subscribers** | The reaction belongs inside the Spree application itself |
289
25
 
290
- ### From Anywhere
26
+ If you're building a headless storefront, **webhooks are almost certainly what you want**. Subscribers are for when you're running Spree as your own application and want to add behaviour to it.
291
27
 
292
- Use `Spree::Events.publish` directly:
28
+ ## What Spree announces
293
29
 
294
- ```ruby
295
- Spree::Events.publish(
296
- 'inventory.low_stock',
297
- { variant_id: variant.id, quantity: variant.total_on_hand }
298
- )
299
- ```
30
+ Most records announce their own lifecycle:
300
31
 
301
- ## Event Serializers
32
+ | Event | When |
33
+ |---|---|
34
+ | `<resource>.created` | A record is created |
35
+ | `<resource>.updated` | It changes |
36
+ | `<resource>.deleted` | It's removed |
302
37
 
303
- Event payloads are generated using the same [Store API V3 serializers](../../api-reference/store-api/introduction.md) used by the REST API. This means webhook payloads and API responses share the same schema, making it easy to reuse types in your integrations.
38
+ Those exist for orders, carts, products, variants, customers, payments, fulfillments, returns, media, price changes and more.
304
39
 
305
- ### How Serializers Work
40
+ On top of that, meaningful business moments get their own events:
306
41
 
307
- When a model publishes an event, Spree looks for a V3 serializer class matching the model name:
42
+ | Event | Meaning |
43
+ |---|---|
44
+ | `order.placed` | A customer completed checkout |
45
+ | `order.paid` | Payment is settled in full |
46
+ | `order.canceled` | The order was cancelled |
47
+ | `order.shipped` / `order.delivered` | Everything has shipped / arrived |
48
+ | `payment.completed` / `payment.voided` | A payment succeeded / was released |
49
+ | `fulfillment.shipped` / `fulfillment.canceled` | A parcel went out / was stood down |
50
+ | `product.out_of_stock` / `product.back_in_stock` | Availability flipped |
51
+ | `return.received` / `return.refunded` | A return arrived / was refunded |
52
+ | `import.completed` / `export.completed` | Bulk work finished |
308
53
 
309
- - `Spree::Order` `Spree::Api::V3::OrderSerializer`
310
- - `Spree::Product` → `Spree::Api::V3::ProductSerializer`
311
- - `Spree::Payment` → `Spree::Api::V3::PaymentSerializer`
54
+ The distinction matters when choosing what to listen for. `order.updated` fires whenever anything about an order changes — including an admin editing a note. `order.placed` fires once, when a customer actually bought something. Sending a confirmation email on the wrong one is how customers receive nine copies.
312
55
 
313
- For STI models (e.g., `Spree::Exports::Products`), the serializer lookup walks up the class hierarchy until it finds a match (e.g., → `Spree::Api::V3::ExportSerializer`).
56
+ ## What an event carries
314
57
 
315
- If no serializer is found, a minimal fallback payload is returned:
58
+ An event carries the record it's about, serialized:
316
59
 
317
60
  ```json
318
- { "id": "prod_86Rf07xd4z", "created_at": "2025-01-15T10:00:00Z", "updated_at": "2025-01-15T10:30:00Z" }
61
+ {
62
+ "id": "or_86Rf07xd4z",
63
+ "number": "R123456789",
64
+ "status": "placed",
65
+ "payment_status": "paid",
66
+ "total": "135.60",
67
+ "email": "customer@example.com"
68
+ }
319
69
  ```
320
70
 
321
- ### Built-in Serializers
322
-
323
- Spree includes V3 serializers for all core models in [`api/app/serializers/spree/api/v3/`](https://github.com/spree/spree/tree/main/api/app/serializers/spree/api/v3):
324
-
325
- | Serializer | Model |
326
- |------------|-------|
327
- | `OrderSerializer` | Orders with totals, statuses, nested line items, fulfillments, payments, addresses |
328
- | `ProductSerializer` | Products with pricing, stock status, availability |
329
- | `PaymentSerializer` | Payments with amounts, states, nested payment method and source |
330
- | `FulfillmentSerializer` | Fulfillments (shipments) with tracking, nested delivery method and delivery rates |
331
- | `LineItemSerializer` | Line items with quantity, pricing, nested option values |
332
- | `VariantSerializer` | Variants with SKU, pricing, nested option values |
333
- | `PriceSerializer` | Prices with amounts, currency, price list |
334
- | ... | [And many more](https://github.com/spree/spree/tree/main/api/app/serializers/spree/api/v3) |
71
+ IDs are the same prefixed IDs the API uses, so you can take an ID out of an event and fetch the full record without translating anything.
335
72
 
336
- ### Payload Context
73
+ ## Reacting in another system
337
74
 
338
- Event serializers receive specific context parameters that control what data is included:
75
+ Point a webhook at your endpoint and subscribe it to the events you care about. That's covered fully in [Webhooks](webhooks.md) — including signature verification, which you should not skip.
339
76
 
340
- - **`store`** Prefers the resource's store (e.g., `order.store`), falls back to `Spree::Current.store`
341
- - **`currency`** — Uses `Spree::Current.currency` (with full fallback chain)
342
- - **`user: nil`** — Events never include user-specific pricing
343
- - **`includes: []`** — Conditional associations are not included in event payloads
77
+ ## Reacting inside Spree
344
78
 
345
- This means event payloads contain the same top-level attributes and unconditional associations as API responses, but conditional associations (like product variants, media, or custom fields) are excluded.
346
-
347
- ### Overriding Event Serializers
348
-
349
- To customize the payload for existing events, create a custom V3 serializer and configure it via [dependencies](../customization/dependencies.md):
350
-
351
- ```ruby app/serializers/my_app/order_serializer.rb
352
- module MyApp
353
- class OrderSerializer < Spree::Api::V3::OrderSerializer
354
- # Add custom attributes
355
- attribute :loyalty_points do |order|
356
- (order.total.to_f * 10).to_i
357
- end
358
-
359
- attribute :custom_field do |order|
360
- order.custom_field
361
- end
362
- end
363
- end
364
- ```
79
+ A subscriber is a small class that names the events it wants and does something when one arrives.
365
80
 
366
- ```ruby config/initializers/spree.rb
367
- Spree.api.order_serializer = 'MyApp::OrderSerializer'
81
+ ```bash
82
+ bin/rails g spree:subscriber OrderPlaced order.placed
368
83
  ```
369
84
 
370
- > **WARNING:** When overriding serializers, make sure to include all attributes that webhooks and subscribers depend on. Removing attributes may break integrations.
371
-
372
- ### Serializers for Custom Models
85
+ The generator writes the class, a test, and the step that's easy to forget registers it.
373
86
 
374
- If you add a custom model that publishes events, create a V3 serializer:
87
+ ```ruby app/subscribers/order_placed_subscriber.rb
88
+ class OrderPlacedSubscriber < Spree::Subscriber
89
+ subscribes_to 'order.placed'
375
90
 
376
- ```ruby app/models/spree/subscription.rb
377
- module Spree
378
- class Subscription < Spree.base_class
379
- publishes_lifecycle_events
380
-
381
- def renew!
382
- update!(renewed_at: Time.current)
383
- publish_event('subscription.renewed')
384
- end
385
- end
386
- end
387
- ```
91
+ def handle(event)
92
+ order = Spree::Order.find_by_prefix_id(event.payload['id'])
93
+ return unless order
388
94
 
389
- ```ruby app/serializers/spree/api/v3/subscription_serializer.rb
390
- module Spree
391
- module Api
392
- module V3
393
- class SubscriptionSerializer < BaseSerializer
394
- typelize plan_name: :string, status: :string,
395
- user_id: [:string, nullable: true],
396
- renewed_at: [:string, nullable: true],
397
- expires_at: [:string, nullable: true]
398
-
399
- attributes :plan_name, :status,
400
- renewed_at: :iso8601, expires_at: :iso8601,
401
- created_at: :iso8601, updated_at: :iso8601
402
-
403
- attribute :user_id do |subscription|
404
- subscription.user&.prefixed_id
405
- end
406
- end
407
- end
95
+ WarehouseClient.new.submit(order)
408
96
  end
409
97
  end
410
98
  ```
411
99
 
412
- Models without a matching serializer will use a minimal fallback payload containing only `id`, `created_at`, and `updated_at`.
100
+ A subscriber can listen to several events, or to a whole family:
413
101
 
414
- ## Registering Subscribers
415
-
416
- Subscribers are not auto-discovered — every subscriber must be registered explicitly, regardless of where the class lives. Add it to the `Spree.subscribers` array in `config/initializers/spree.rb` (or any initializer):
417
-
418
- ```ruby config/initializers/spree.rb
419
- Rails.application.config.after_initialize do
420
- Spree.subscribers << CustomSubscriber
421
- end
102
+ ```ruby
103
+ subscribes_to 'order.placed', 'order.canceled'
104
+ subscribes_to 'order.*'
422
105
  ```
423
106
 
424
- To remove a built-in subscriber:
107
+ Subscribers must be registered — they aren't discovered automatically, because a subscriber that starts running because of where its file sits is hard to reason about:
425
108
 
426
109
  ```ruby config/initializers/spree.rb
427
110
  Rails.application.config.after_initialize do
428
- Spree.subscribers.delete(Spree::ExportSubscriber)
111
+ Spree.subscribers << OrderPlacedSubscriber
429
112
  end
430
113
  ```
431
114
 
432
- ## Synchronous vs Asynchronous
115
+ ### Subscribers run in the background
433
116
 
434
- By default, subscribers run asynchronously via a background job. This prevents slow subscriber code from blocking HTTP requests.
117
+ By default a subscriber runs as a background job, so a slow API call in your code doesn't slow down the customer's checkout — and a failure doesn't roll back their order.
435
118
 
436
- For critical operations that must complete before the request finishes, use synchronous mode:
119
+ That's almost always what you want. If you genuinely need to run inside the same transaction, you can ask to:
437
120
 
438
121
  ```ruby
439
- class CriticalOrderHandler < Spree::Subscriber
440
- subscribes_to 'order.completed', async: false
441
-
442
- def handle(event)
443
- # This runs immediately, blocking the request
444
- end
445
- end
122
+ subscribes_to 'order.placed', async: false
446
123
  ```
447
124
 
448
- > **WARNING:** Use synchronous subscribers sparingly. They can significantly slow down your application if the handler code is slow or makes external API calls.
125
+ > **WARNING:** A synchronous subscriber runs while the customer waits, and an exception in it can fail their checkout. Reserve it for work that must be atomic with the order, and keep it fast.
449
126
 
450
- ## Temporarily Disabling Events
127
+ ## Publishing your own events
451
128
 
452
- You can disable event publishing temporarily:
129
+ Anything in your own code can announce something, and subscribers and webhooks treat it like any built-in event:
453
130
 
454
131
  ```ruby
455
- Spree::Events.disable do
456
- # Events published in this block won't trigger subscribers
457
- order.complete!
458
- end
459
- ```
460
-
461
- This is useful for:
462
- - Data migrations where you don't want to trigger side effects
463
- - Test setup where subscribers would interfere
464
- - Bulk operations where individual events would be too noisy
465
-
466
- ## Testing Subscribers
467
-
468
- ### Testing Event Handling
469
-
470
- ```ruby spec/subscribers/order_completed_subscriber_spec.rb
471
- require 'spec_helper'
472
-
473
- RSpec.describe OrderCompletedSubscriber do
474
- let(:order) { create(:completed_order_with_totals) }
475
- let(:event) do
476
- Spree::Event.new(
477
- name: 'order.completed',
478
- payload: order.event_payload
479
- )
480
- end
481
-
482
- describe '#handle' do
483
- it 'notifies external service' do
484
- expect(ExternalService).to receive(:notify_order_placed).with(order)
485
- described_class.new.handle(event)
486
- end
487
- end
488
- end
132
+ order.publish_event('order.flagged_for_review')
489
133
  ```
490
134
 
491
- ### Testing Event Publishing
492
-
493
- Stub `Spree::Events.publish` to assert an event is published:
494
-
495
- ```ruby
496
- it 'publishes order.completed event' do
497
- expect(Spree::Events).to receive(:publish).with(
498
- 'order.completed',
499
- hash_including('id' => order.id)
500
- )
501
-
502
- order.complete!
503
- end
504
- ```
505
-
506
- The `lifecycle events` shared examples in `spree/core/lib/spree/testing_support/lifecycle_events.rb` cover the standard `created`/`updated`/`deleted` lifecycle events.
507
-
508
- ## Best Practices
509
-
510
-
511
- - **Keep handlers fast** — Move slow operations to background jobs. Subscribers should do minimal work and delegate heavy lifting.
512
-
513
- - **Handle missing records** — Always check if the record exists before processing. It may have been deleted between event publish and handler execution.
514
-
515
- - **Be idempotent** — Design handlers to be safely re-run. Events might be delivered more than once in edge cases.
516
-
517
- - **Use specific patterns** — Subscribe to specific events rather than wildcards when possible. This makes code easier to understand and debug.
518
-
519
-
520
- ## Example: Inventory Alert Subscriber
521
-
522
- Here's a complete example of a subscriber that sends alerts when inventory is low:
523
-
524
- ```ruby app/subscribers/inventory_alert_subscriber.rb
525
- class InventoryAlertSubscriber < Spree::Subscriber
526
- subscribes_to 'stock_item.updated'
527
-
528
- LOW_STOCK_THRESHOLD = 10
529
-
530
- def handle(event)
531
- stock_item = find_stock_item(event)
532
- return unless stock_item
533
- return unless low_stock?(event)
534
-
535
- send_low_stock_alert(stock_item)
536
- end
537
-
538
- private
539
-
540
- def find_stock_item(event)
541
- Spree::StockItem.find_by_prefix_id(event.payload['id'])
542
- end
543
-
544
- def low_stock?(event)
545
- event.payload['count_on_hand'].to_i < LOW_STOCK_THRESHOLD
546
- end
547
-
548
- def send_low_stock_alert(stock_item)
549
- InventoryMailer.low_stock_alert(
550
- variant: stock_item.variant,
551
- stock_location: stock_item.stock_location,
552
- count_on_hand: stock_item.count_on_hand
553
- ).deliver_later
554
- end
555
- end
556
- ```
557
-
558
- > **NOTE:** A lifecycle event payload carries only the resource's current serialized state, not its previous values. If you need a true "just dropped below the threshold" check that compares the count before and after the change, record or cache the prior count separately rather than reading it from the event payload.
559
-
560
- ## Custom Event Adapters
561
-
562
- Spree's event system uses an adapter pattern, making it possible to swap the underlying event infrastructure. By default, Spree uses [`ActiveSupport::Notifications`](https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html), but you can create custom adapters for other backends like Kafka, RabbitMQ, or Redis Pub/Sub.
563
-
564
- ### Configuring a Custom Adapter
565
-
566
- Set your adapter class in an initializer:
567
-
568
- ```ruby config/initializers/spree.rb
569
- Spree.events_adapter_class = 'MyApp::Events::KafkaAdapter'
570
- ```
571
-
572
- ### Creating a Custom Adapter
573
-
574
- Inherit from `Spree::Events::Adapters::Base` and implement the required methods:
575
-
576
- ```ruby app/models/my_app/events/kafka_adapter.rb
577
- module MyApp
578
- module Events
579
- class KafkaAdapter < Spree::Events::Adapters::Base
580
- def publish(event_name, payload, metadata = {})
581
- event = build_event(event_name, payload, metadata)
582
-
583
- # Publish to Kafka
584
- kafka_producer.produce(
585
- event.to_json,
586
- topic: "spree.#{event_name}"
587
- )
588
-
589
- event
590
- end
591
-
592
- def subscribe(pattern, subscriber, options = {})
593
- registry.register(pattern, subscriber, options)
594
- end
595
-
596
- def unsubscribe(pattern, subscriber)
597
- registry.unregister(pattern, subscriber)
598
- end
599
-
600
- def activate!
601
- @kafka_producer = Kafka.new(
602
- seed_brokers: ENV['KAFKA_BROKERS']
603
- ).producer
604
- end
605
-
606
- def deactivate!
607
- @kafka_producer&.shutdown
608
- end
609
-
610
- private
611
-
612
- attr_reader :kafka_producer
613
- end
614
- end
615
- end
616
- ```
135
+ Useful when your own domain has moments worth reacting to — a fraud check completing, an approval granted.
617
136
 
618
- ### Base Class Interface
137
+ ## Guidance
619
138
 
620
- The `Spree::Events::Adapters::Base` class defines the required interface:
139
+ **Listen for the specific event.** `order.placed` rather than `order.updated` with a status check.
621
140
 
622
- | Method | Description |
623
- |--------|-------------|
624
- | `publish(event_name, payload, metadata)` | Publish an event, return `Spree::Event` |
625
- | `subscribe(pattern, subscriber, options)` | Register a subscriber for a pattern |
626
- | `unsubscribe(pattern, subscriber)` | Remove a subscriber |
627
- | `activate!` | Called during application initialization |
628
- | `deactivate!` | Called during shutdown |
141
+ **Assume events can arrive more than once.** A retry after a network blip can redeliver. Make handlers safe to run twice — check whether you've already acted before acting.
629
142
 
630
- The base class also provides helper methods:
631
- - `build_event(name, payload, metadata)` - Creates a `Spree::Event` instance
632
- - `invoke_subscribers(event)` - Finds and invokes matching subscribers (internally calling `registry.subscriptions_for`)
633
- - `registry` - Access to the `Spree::Events::Registry` instance
143
+ **Don't chain long sequences of subscribers.** When one event triggers a subscriber that triggers another, working out what happened after the fact becomes archaeology. Prefer one handler that does the sequence.
634
144
 
635
- > **INFO:** See `Spree::Events::Adapters::ActiveSupportNotifications` for a complete reference implementation.
145
+ **Keep failures contained.** A subscriber that raises shouldn't take down anything else. Handle your own errors and log them.
636
146
 
637
- ## Related Documentation
147
+ ## Related
638
148
 
639
- - [Events Tutorial](../tutorial/events.md) - Hands-on, step-by-step walkthrough of building an event subscriber
640
- - [Webhooks](webhooks.md) - HTTP callbacks for external integrations
641
- - [Webhooks & Events Reference](../../api-reference/webhooks-events.md) - Catalog of event and webhook payloads
642
- - [Customization Quickstart](../customization/quickstart.md) - Overview of all customization options
643
- - [Decorators](../customization/decorators.md) - When to use decorators vs events
644
- - [Checkout Flow](carts.md) - Using events in checkout customization
149
+ - [Webhooks](webhooks.md) reacting from outside Spree
150
+ - [Orders](orders.md) the order lifecycle these events describe
151
+ - [Imports & Exports](imports-exports.md) completion events for bulk work