@spree/docs 0.1.182 → 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.
- package/dist/developer/core-concepts/addresses.md +106 -198
- package/dist/developer/core-concepts/architecture.md +97 -126
- package/dist/developer/core-concepts/calculators.md +75 -252
- package/dist/developer/core-concepts/carts.md +1 -1
- package/dist/developer/core-concepts/channels.md +0 -4
- package/dist/developer/core-concepts/companies-and-catalogs.md +1 -1
- package/dist/developer/core-concepts/customers.md +0 -3
- package/dist/developer/core-concepts/discounts.md +133 -0
- package/dist/developer/core-concepts/events.md +83 -576
- package/dist/developer/core-concepts/fees.md +144 -0
- package/dist/developer/core-concepts/imports-exports.md +105 -679
- package/dist/developer/core-concepts/inventory.md +114 -248
- package/dist/developer/core-concepts/markets.md +9 -12
- package/dist/developer/core-concepts/media.md +9 -11
- package/dist/developer/core-concepts/metafields.md +123 -200
- package/dist/developer/core-concepts/order-totals.md +110 -0
- package/dist/developer/core-concepts/orders.md +1 -1
- package/dist/developer/core-concepts/payments.md +11 -14
- package/dist/developer/core-concepts/pricing.md +11 -13
- package/dist/developer/core-concepts/products.md +35 -19
- package/dist/developer/core-concepts/promotions.md +12 -11
- package/dist/developer/core-concepts/search-filtering.md +2 -4
- package/dist/developer/core-concepts/store-credits-gift-cards.md +0 -3
- package/dist/developer/core-concepts/taxes.md +125 -113
- package/dist/developer/core-concepts/translations.md +61 -68
- package/dist/developer/core-concepts/webhooks.md +25 -59
- package/dist/developer/how-to/custom-promotion.md +3 -3
- package/package.json +1 -1
- package/dist/developer/core-concepts/taxes-discounts-fees.md +0 -199
|
@@ -1,741 +1,167 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Imports & Exports
|
|
3
|
+
description: Moving data in and out of Spree as CSV — bulk catalog updates, customer lists, and order extracts.
|
|
3
4
|
---
|
|
4
5
|
|
|
5
6
|
## Overview
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Spreadsheets remain how most merchants think about bulk work. A supplier sends a price list as CSV; the finance team wants last quarter's orders in Excel; a migration from another platform arrives as one enormous product export.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Spree handles both directions as background work, so a hundred-thousand-row file doesn't tie up a browser tab or time out.
|
|
10
11
|
|
|
11
12
|
```mermaid
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
bigint user_id
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
ImportMapping {
|
|
23
|
-
string schema_field
|
|
24
|
-
string file_column
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
ImportRow {
|
|
28
|
-
integer row_number
|
|
29
|
-
text data
|
|
30
|
-
string status
|
|
31
|
-
text validation_errors
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
ImportSchema {
|
|
35
|
-
array fields
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
RowProcessor {
|
|
39
|
-
hash attributes
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
Export {
|
|
43
|
-
string number
|
|
44
|
-
string type
|
|
45
|
-
integer format
|
|
46
|
-
jsonb search_params
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
Store {
|
|
50
|
-
string name
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
Import ||--|| Store : "belongs to (owner)"
|
|
54
|
-
Import ||--o{ ImportMapping : "has many"
|
|
55
|
-
Import ||--o{ ImportRow : "has many"
|
|
56
|
-
Import ||--|| ImportSchema : "uses"
|
|
57
|
-
ImportRow ||--|| RowProcessor : "processed by"
|
|
58
|
-
Export ||--|| Store : "belongs to"
|
|
13
|
+
flowchart LR
|
|
14
|
+
File["CSV file"] --> Upload["Upload"]
|
|
15
|
+
Upload --> Map["Match columns<br/>to fields"]
|
|
16
|
+
Map --> Process["Process rows"]
|
|
17
|
+
Process --> Good["Imported"]
|
|
18
|
+
Process --> Bad["Failed rows<br/>with reasons"]
|
|
19
|
+
Bad --> Retry["Fix and retry"]
|
|
59
20
|
```
|
|
60
21
|
|
|
61
|
-
##
|
|
22
|
+
## What can be imported and exported
|
|
62
23
|
|
|
63
|
-
|
|
24
|
+
| Data | Import | Export |
|
|
25
|
+
|---|:---:|:---:|
|
|
26
|
+
| Products | Yes | Yes |
|
|
27
|
+
| Product translations | Yes | Yes |
|
|
28
|
+
| Customers | Yes | Yes |
|
|
29
|
+
| Orders | — | Yes |
|
|
30
|
+
| Gift cards | — | Yes |
|
|
31
|
+
| Coupon codes | — | Yes |
|
|
32
|
+
| Newsletter subscribers | — | Yes |
|
|
64
33
|
|
|
65
|
-
|
|
66
|
-
2. **State Machine**: Imports progress through states (pending → mapping → processing → completed)
|
|
67
|
-
3. **Schema Definition**: ImportSchema classes define expected fields and validation
|
|
68
|
-
4. **Row Processors**: Transform CSV rows into database records
|
|
69
|
-
5. **Event-Driven Processing**: Background jobs handle heavy lifting asynchronously
|
|
70
|
-
6. **Registry Pattern**: Types registered in `Spree.import_types` and `Spree.export_types`
|
|
34
|
+
Orders are export-only on purpose. An order is a financial record of something that happened; inventing them from a spreadsheet would let the books say something that never occurred.
|
|
71
35
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
## Exports
|
|
75
|
-
|
|
76
|
-
Exports generate CSV files from filtered database records.
|
|
77
|
-
|
|
78
|
-
### Built-in Export Types
|
|
79
|
-
|
|
80
|
-
| Type | Description | Multi-line |
|
|
81
|
-
|------|-------------|------------|
|
|
82
|
-
| `Spree::Exports::Products` | Products with all variants | Yes |
|
|
83
|
-
| `Spree::Exports::ProductTranslations` | Product translations for non-default store locales | Yes |
|
|
84
|
-
| `Spree::Exports::Orders` | Orders with line items | Yes |
|
|
85
|
-
| `Spree::Exports::Customers` | Customer accounts | No |
|
|
86
|
-
| `Spree::Exports::GiftCards` | Gift cards | No |
|
|
87
|
-
| `Spree::Exports::NewsletterSubscribers` | Newsletter subscribers | No |
|
|
88
|
-
| `Spree::Exports::CouponCodes` | Promotion coupon codes | No |
|
|
89
|
-
|
|
90
|
-
### Export Model
|
|
91
|
-
|
|
92
|
-
The base `Spree::Export` class provides:
|
|
93
|
-
|
|
94
|
-
```ruby
|
|
95
|
-
module Spree
|
|
96
|
-
class Export < Spree.base_class
|
|
97
|
-
# Associations
|
|
98
|
-
belongs_to :store
|
|
99
|
-
belongs_to :user # Admin who created export
|
|
100
|
-
|
|
101
|
-
# Attachments
|
|
102
|
-
has_one_attached :attachment # Generated CSV file
|
|
103
|
-
|
|
104
|
-
# Key methods
|
|
105
|
-
def csv_headers # Define column headers
|
|
106
|
-
def scope # Base query with store/vendor filtering
|
|
107
|
-
def scope_includes # Eager loading associations
|
|
108
|
-
def records_to_export # Apply ransack filters
|
|
109
|
-
def multi_line_csv? # True if records produce multiple rows
|
|
110
|
-
def generate # Create CSV and attach file
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### Creating a Custom Exporter
|
|
116
|
-
|
|
117
|
-
**Step 1: Create the Export Class**
|
|
118
|
-
|
|
119
|
-
```ruby app/models/spree/exports/subscriptions.rb
|
|
120
|
-
module Spree
|
|
121
|
-
module Exports
|
|
122
|
-
class Subscriptions < Spree::Export
|
|
123
|
-
# Define CSV column headers
|
|
124
|
-
def csv_headers
|
|
125
|
-
%w[id email plan_name status created_at] + metafields_headers
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# Eager load associations to avoid N+1 queries
|
|
129
|
-
def scope_includes
|
|
130
|
-
[:user, :plan, { metafields: :metafield_definition }]
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Override scope if needed (e.g., exclude cancelled)
|
|
134
|
-
def scope
|
|
135
|
-
super.where.not(status: 'cancelled')
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Set to true if each record produces multiple CSV rows
|
|
139
|
-
def multi_line_csv?
|
|
140
|
-
false
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
end
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
**Step 2: Add `to_csv` Method to Your Model**
|
|
148
|
-
|
|
149
|
-
```ruby app/models/spree/subscription.rb
|
|
150
|
-
module Spree
|
|
151
|
-
class Subscription < Spree.base_class
|
|
152
|
-
def to_csv(store)
|
|
153
|
-
[
|
|
154
|
-
id,
|
|
155
|
-
user&.email,
|
|
156
|
-
plan&.name,
|
|
157
|
-
status,
|
|
158
|
-
created_at.iso8601
|
|
159
|
-
] + metafields_csv_values(store)
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
private
|
|
163
|
-
|
|
164
|
-
def metafields_csv_values(store)
|
|
165
|
-
Spree::MetafieldDefinition.for_resource_type(self.class.name).order(:namespace, :key).map do |definition|
|
|
166
|
-
metafields.find { |m| m.metafield_definition_id == definition.id }&.value
|
|
167
|
-
end
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
**Step 3: Register the Export Type**
|
|
174
|
-
|
|
175
|
-
```ruby config/initializers/spree.rb
|
|
176
|
-
Rails.application.config.after_initialize do
|
|
177
|
-
Spree.export_types << Spree::Exports::Subscriptions
|
|
178
|
-
end
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
**Step 4: Add Translations**
|
|
182
|
-
|
|
183
|
-
```yaml config/locales/en.yml
|
|
184
|
-
en:
|
|
185
|
-
spree:
|
|
186
|
-
subscriptions: Subscriptions
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Multi-line Exports
|
|
190
|
-
|
|
191
|
-
For exports where each record produces multiple rows (like products with variants):
|
|
192
|
-
|
|
193
|
-
```ruby app/models/spree/exports/orders_with_items.rb
|
|
194
|
-
module Spree
|
|
195
|
-
module Exports
|
|
196
|
-
class OrdersWithItems < Spree::Export
|
|
197
|
-
def multi_line_csv?
|
|
198
|
-
true
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def csv_headers
|
|
202
|
-
%w[order_number line_item_sku quantity price]
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def scope_includes
|
|
206
|
-
[line_items: :variant]
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
```ruby app/models/spree/order.rb
|
|
214
|
-
# In the Order model
|
|
215
|
-
def to_csv(store)
|
|
216
|
-
line_items.map do |item|
|
|
217
|
-
[number, item.variant.sku, item.quantity, item.price]
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
### Export Filtering
|
|
223
|
-
|
|
224
|
-
Exports support [Ransack filtering via `search_params`](../../api-reference/admin-api/querying.md):
|
|
225
|
-
|
|
226
|
-
```ruby
|
|
227
|
-
# In admin, users can filter before exporting
|
|
228
|
-
export = Spree::Exports::Products.new(
|
|
229
|
-
store: current_store,
|
|
230
|
-
user: current_user,
|
|
231
|
-
search_params: { name_cont: 'shirt', status_eq: 'active' }.to_json,
|
|
232
|
-
record_selection: 'filtered' # or 'all' to ignore filters
|
|
233
|
-
)
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
|
-
## Imports
|
|
239
|
-
|
|
240
|
-
Imports process CSV files to create or update database records.
|
|
241
|
-
|
|
242
|
-
### Built-in Import Types
|
|
243
|
-
|
|
244
|
-
| Type | Description |
|
|
245
|
-
|------|-------------|
|
|
246
|
-
| `Spree::Imports::Products` | Products and variants |
|
|
247
|
-
| `Spree::Imports::ProductTranslations` | Product translations (matched by slug) |
|
|
248
|
-
| `Spree::Imports::Customers` | Customers |
|
|
249
|
-
|
|
250
|
-
### Import Workflow
|
|
251
|
-
|
|
252
|
-
```
|
|
253
|
-
1. Upload CSV → pending
|
|
254
|
-
2. Auto-map columns → mapping
|
|
255
|
-
3. User confirms mapping → completed_mapping
|
|
256
|
-
4. Parse rows (CreateRowsJob) → processing
|
|
257
|
-
5. Process rows (ProcessRowsJob) → completed/failed
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### Import Components
|
|
261
|
-
|
|
262
|
-
#### Import Model
|
|
263
|
-
|
|
264
|
-
```ruby
|
|
265
|
-
module Spree
|
|
266
|
-
class Import < Spree.base_class
|
|
267
|
-
# Associations
|
|
268
|
-
belongs_to :owner, polymorphic: true # Store or Vendor
|
|
269
|
-
belongs_to :user
|
|
270
|
-
has_many :mappings # Field mappings
|
|
271
|
-
has_many :rows # CSV rows to process
|
|
272
|
-
|
|
273
|
-
# Statuses. Each move between them is a workflow — Spree::Imports::
|
|
274
|
-
# StartMapping, CompleteMapping, StartProcessing, Complete and
|
|
275
|
-
# RetryFailedRows — so every step of the pipeline can be hooked.
|
|
276
|
-
include Spree::HasStatus
|
|
277
|
-
has_status :pending, :mapping, :completed_mapping, :processing, :completed, :failed,
|
|
278
|
-
default: :pending
|
|
279
|
-
|
|
280
|
-
# Key methods
|
|
281
|
-
def import_schema # Returns schema class instance
|
|
282
|
-
def row_processor_class # Returns processor class
|
|
283
|
-
def schema_fields # Fields from schema + metafields
|
|
284
|
-
def mapping_done? # All required fields mapped?
|
|
285
|
-
end
|
|
286
|
-
end
|
|
287
|
-
```
|
|
36
|
+
## Exporting
|
|
288
37
|
|
|
289
|
-
|
|
38
|
+
An export is created, runs in the background, and produces a file to download.
|
|
290
39
|
|
|
291
|
-
Defines expected CSV fields:
|
|
292
40
|
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
41
|
+
```typescript Admin SDK
|
|
42
|
+
const exportJob = await adminClient.exports.create({
|
|
43
|
+
type: 'Spree::Exports::Products',
|
|
44
|
+
})
|
|
297
45
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def required_fields
|
|
303
|
-
FIELDS.select { |f| f[:required] }.map { |f| f[:name] }
|
|
304
|
-
end
|
|
305
|
-
|
|
306
|
-
def optional_fields
|
|
307
|
-
FIELDS.reject { |f| f[:required] }.map { |f| f[:name] }
|
|
308
|
-
end
|
|
309
|
-
end
|
|
310
|
-
end
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
#### Import Mapping
|
|
314
|
-
|
|
315
|
-
Maps CSV columns to schema fields:
|
|
316
|
-
|
|
317
|
-
```ruby
|
|
318
|
-
module Spree
|
|
319
|
-
class ImportMapping < Spree.base_class
|
|
320
|
-
belongs_to :import
|
|
321
|
-
|
|
322
|
-
# Attributes
|
|
323
|
-
# schema_field - target field name from schema
|
|
324
|
-
# file_column - CSV column header
|
|
325
|
-
|
|
326
|
-
def try_to_auto_assign_file_column(csv_headers)
|
|
327
|
-
# Matches by parameterized name comparison
|
|
328
|
-
self.file_column = csv_headers.find do |header|
|
|
329
|
-
header.parameterize.underscore == schema_field.parameterize.underscore
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
end
|
|
46
|
+
// Poll until it's done
|
|
47
|
+
const status = await adminClient.exports.get(exportJob.id)
|
|
48
|
+
status.status // "pending" → "processing" → "completed"
|
|
49
|
+
status.download_url // available once completed
|
|
334
50
|
```
|
|
335
51
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
```ruby
|
|
341
|
-
module Spree
|
|
342
|
-
class ImportRow < Spree.base_class
|
|
343
|
-
belongs_to :import, counter_cache: :rows_count
|
|
344
|
-
belongs_to :item, polymorphic: true, optional: true # Created record
|
|
345
|
-
|
|
346
|
-
# Attributes
|
|
347
|
-
# row_number - position in CSV
|
|
348
|
-
# data - JSON-serialized row data
|
|
349
|
-
# status - pending/processing/completed/failed
|
|
350
|
-
# validation_errors - error message if failed
|
|
351
|
-
|
|
352
|
-
def process!
|
|
353
|
-
start_processing!
|
|
354
|
-
self.item = import.row_processor_class.new(self).process!
|
|
355
|
-
complete!
|
|
356
|
-
rescue StandardError => e
|
|
357
|
-
self.validation_errors = e.message
|
|
358
|
-
fail!
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
def to_schema_hash
|
|
362
|
-
# Maps CSV data using import.mappings
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
end
|
|
52
|
+
```bash CLI
|
|
53
|
+
spree api post /exports -d '{"type": "Spree::Exports::Products"}'
|
|
54
|
+
spree api get /exports
|
|
366
55
|
```
|
|
367
56
|
|
|
368
|
-
#### Row Processor
|
|
369
|
-
|
|
370
|
-
Transforms row data into database records:
|
|
371
|
-
|
|
372
|
-
```ruby
|
|
373
|
-
module Spree
|
|
374
|
-
module Imports
|
|
375
|
-
module RowProcessors
|
|
376
|
-
class Base
|
|
377
|
-
def initialize(row)
|
|
378
|
-
@row = row
|
|
379
|
-
@import = row.import
|
|
380
|
-
@attributes = row.to_schema_hash
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
attr_reader :row, :import, :attributes
|
|
384
|
-
|
|
385
|
-
def process!
|
|
386
|
-
raise NotImplementedError
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
end
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
```
|
|
393
57
|
|
|
394
|
-
###
|
|
58
|
+
### Exporting a filtered set
|
|
395
59
|
|
|
396
|
-
|
|
60
|
+
An export can carry the same filters as the listing it came from, so "export what I'm looking at" does what a merchant expects — this quarter's orders, this brand's products, not the entire table.
|
|
397
61
|
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
Spree::Imports::RowProcessors::Subscription
|
|
404
|
-
end
|
|
405
|
-
end
|
|
406
|
-
end
|
|
407
|
-
end
|
|
62
|
+
```typescript Admin SDK
|
|
63
|
+
await adminClient.exports.create({
|
|
64
|
+
type: 'Spree::Exports::Orders',
|
|
65
|
+
filters: { completed_at_gteq: '2026-01-01' },
|
|
66
|
+
})
|
|
408
67
|
```
|
|
409
68
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
```ruby app/models/spree/import_schemas/subscriptions.rb
|
|
413
|
-
module Spree
|
|
414
|
-
module ImportSchemas
|
|
415
|
-
class Subscriptions < Spree::ImportSchema
|
|
416
|
-
FIELDS = [
|
|
417
|
-
{ name: 'email', label: 'Customer Email', required: true },
|
|
418
|
-
{ name: 'plan_name', label: 'Plan Name', required: true },
|
|
419
|
-
{ name: 'status', label: 'Status', required: true },
|
|
420
|
-
{ name: 'start_date', label: 'Start Date' },
|
|
421
|
-
{ name: 'billing_interval', label: 'Billing Interval' },
|
|
422
|
-
{ name: 'amount', label: 'Amount' },
|
|
423
|
-
{ name: 'currency', label: 'Currency' }
|
|
424
|
-
].freeze
|
|
425
|
-
end
|
|
426
|
-
end
|
|
427
|
-
end
|
|
428
|
-
```
|
|
69
|
+
In the dashboard this is the Export button on any list — whatever filters are applied come along.
|
|
429
70
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
```ruby app/services/spree/imports/row_processors/subscription.rb
|
|
433
|
-
module Spree
|
|
434
|
-
module Imports
|
|
435
|
-
module RowProcessors
|
|
436
|
-
class Subscription < Base
|
|
437
|
-
def process!
|
|
438
|
-
user = find_or_create_user
|
|
439
|
-
plan = find_plan
|
|
440
|
-
|
|
441
|
-
subscription = Spree::Subscription.find_or_initialize_by(
|
|
442
|
-
user: user,
|
|
443
|
-
plan: plan
|
|
444
|
-
)
|
|
445
|
-
|
|
446
|
-
subscription.status = attributes['status'] if attributes['status'].present?
|
|
447
|
-
subscription.start_date = parse_date(attributes['start_date']) if attributes['start_date'].present?
|
|
448
|
-
subscription.billing_interval = attributes['billing_interval'] if attributes['billing_interval'].present?
|
|
449
|
-
|
|
450
|
-
if attributes['amount'].present?
|
|
451
|
-
currency = attributes['currency'].presence || import.store.default_currency
|
|
452
|
-
subscription.set_price(currency, attributes['amount'])
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
subscription.save!
|
|
456
|
-
subscription
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
private
|
|
460
|
-
|
|
461
|
-
def find_or_create_user
|
|
462
|
-
email = attributes['email'].strip.downcase
|
|
463
|
-
Spree.user_class.find_or_create_by!(email: email)
|
|
464
|
-
end
|
|
465
|
-
|
|
466
|
-
def find_plan
|
|
467
|
-
Spree::Plan.find_by!(name: attributes['plan_name'].strip)
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
def parse_date(date_string)
|
|
471
|
-
Date.parse(date_string)
|
|
472
|
-
rescue ArgumentError
|
|
473
|
-
nil
|
|
474
|
-
end
|
|
475
|
-
end
|
|
476
|
-
end
|
|
477
|
-
end
|
|
478
|
-
end
|
|
479
|
-
```
|
|
71
|
+
## Importing
|
|
480
72
|
|
|
481
|
-
|
|
73
|
+
Importing has an extra step, because a file from somewhere else won't have Spree's column names. You upload it, say which of your columns means what, and then it runs.
|
|
482
74
|
|
|
483
|
-
|
|
484
|
-
Rails.application.config.after_initialize do
|
|
485
|
-
Spree.import_types << Spree::Imports::Subscriptions
|
|
486
|
-
end
|
|
487
|
-
```
|
|
75
|
+
**Step 1: Upload the file**
|
|
488
76
|
|
|
489
|
-
|
|
77
|
+
```typescript Admin SDK
|
|
78
|
+
const importJob = await adminClient.imports.create({
|
|
79
|
+
type: 'Spree::Imports::Products',
|
|
80
|
+
file: uploadedFileId,
|
|
81
|
+
})
|
|
82
|
+
```
|
|
490
83
|
|
|
491
|
-
|
|
492
|
-
en:
|
|
493
|
-
spree:
|
|
494
|
-
subscriptions: Subscriptions
|
|
495
|
-
```
|
|
84
|
+
Spree reads the header row and tells you which columns it found.
|
|
496
85
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
The built-in products import supports these fields:
|
|
500
|
-
|
|
501
|
-
**Required Fields:**
|
|
502
|
-
- `slug` - Product URL slug
|
|
503
|
-
- `sku` - Variant SKU
|
|
504
|
-
- `name` - Product name
|
|
505
|
-
- `price` - Variant price
|
|
506
|
-
|
|
507
|
-
**Optional Fields:**
|
|
508
|
-
- `status` - Product status (active/draft/archived)
|
|
509
|
-
- `description` - Product description
|
|
510
|
-
- `meta_title`, `meta_description`, `meta_keywords` - SEO metadata
|
|
511
|
-
- `tags` - Product tags
|
|
512
|
-
- `compare_at_price` - Original price for sale display
|
|
513
|
-
- `currency` - Price currency
|
|
514
|
-
- `width`, `height`, `depth`, `dimensions_unit` - Dimensions
|
|
515
|
-
- `weight`, `weight_unit` - Weight
|
|
516
|
-
- `available_on`, `discontinue_on` - Availability dates
|
|
517
|
-
- `track_inventory` - Enable inventory tracking
|
|
518
|
-
- `inventory_count`, `inventory_backorderable` - Stock settings
|
|
519
|
-
- `tax_category`, `shipping_category` - Category assignments
|
|
520
|
-
- `image1_src`, `image2_src`, `image3_src` - Image URLs
|
|
521
|
-
- `option1_name`, `option1_value` through `option3_name`, `option3_value` - Variant options
|
|
522
|
-
- `category1`, `category2`, `category3` - Taxon assignments (format: "Taxonomy -> Taxon -> Child Taxon")
|
|
523
|
-
|
|
524
|
-
### Handling Multi-Variant Products
|
|
525
|
-
|
|
526
|
-
The products import handles variants intelligently:
|
|
527
|
-
|
|
528
|
-
1. **Master variant rows** (no option values): Create/update the product and its master variant
|
|
529
|
-
2. **Non-master variant rows** (with option values): Create additional variants for an existing product
|
|
530
|
-
|
|
531
|
-
```csv
|
|
532
|
-
slug,sku,name,price,option1_name,option1_value,option2_name,option2_value
|
|
533
|
-
my-tshirt,TSHIRT-001,My T-Shirt,29.99,,,,
|
|
534
|
-
my-tshirt,TSHIRT-S-RED,My T-Shirt,29.99,Size,Small,Color,Red
|
|
535
|
-
my-tshirt,TSHIRT-M-RED,My T-Shirt,29.99,Size,Medium,Color,Red
|
|
536
|
-
my-tshirt,TSHIRT-L-RED,My T-Shirt,29.99,Size,Large,Color,Red
|
|
537
|
-
```
|
|
86
|
+
**Step 2: Match columns to fields**
|
|
538
87
|
|
|
539
|
-
|
|
88
|
+
```typescript Admin SDK
|
|
89
|
+
await adminClient.imports.completeMapping(importJob.id, {
|
|
90
|
+
mappings: [
|
|
91
|
+
{ schema_field: 'name', file_column: 'Product Title' },
|
|
92
|
+
{ schema_field: 'sku', file_column: 'Item Code' },
|
|
93
|
+
{ schema_field: 'price', file_column: 'RRP' },
|
|
94
|
+
],
|
|
95
|
+
})
|
|
96
|
+
```
|
|
540
97
|
|
|
541
|
-
|
|
98
|
+
Obvious matches are suggested for you; you only correct the ones that differ.
|
|
542
99
|
|
|
543
|
-
**
|
|
100
|
+
**Step 3: Watch it run**
|
|
544
101
|
|
|
545
|
-
|
|
102
|
+
```typescript Admin SDK
|
|
103
|
+
const status = await adminClient.imports.get(importJob.id)
|
|
546
104
|
|
|
547
|
-
|
|
105
|
+
status.status // "processing" → "completed"
|
|
106
|
+
status.rows_count // total
|
|
107
|
+
status.processed_count
|
|
108
|
+
status.failed_count
|
|
109
|
+
```
|
|
548
110
|
|
|
549
|
-
## Background Jobs
|
|
550
111
|
|
|
551
|
-
|
|
112
|
+
> **INFO:** Download a template for any import type to see the expected columns — the quickest way to prepare a file that maps cleanly.
|
|
552
113
|
|
|
553
|
-
|
|
114
|
+
## When rows fail
|
|
554
115
|
|
|
555
|
-
|
|
116
|
+
Some rows will fail. A price with a currency symbol in it, a required field left blank, a duplicate SKU.
|
|
556
117
|
|
|
557
|
-
|
|
558
|
-
module Spree
|
|
559
|
-
module Imports
|
|
560
|
-
class CreateRowsJob < Spree::Imports::BaseJob
|
|
561
|
-
def perform(import_id)
|
|
562
|
-
import = Spree::Import.find(import_id)
|
|
563
|
-
# Stream CSV, batch insert rows
|
|
564
|
-
# Then enqueue ProcessRowsJob via import.process_rows_async
|
|
565
|
-
end
|
|
566
|
-
end
|
|
567
|
-
end
|
|
568
|
-
end
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
### ProcessRowsJob
|
|
572
|
-
|
|
573
|
-
Fans the pending rows out into groups, each processed by a separate `ProcessGroupJob`:
|
|
574
|
-
|
|
575
|
-
```ruby
|
|
576
|
-
module Spree
|
|
577
|
-
module Imports
|
|
578
|
-
class ProcessRowsJob < Spree::Imports::BaseJob
|
|
579
|
-
BATCH_SIZE = 100
|
|
580
|
-
|
|
581
|
-
def perform(import_id)
|
|
582
|
-
import = Spree::Import.find(import_id)
|
|
583
|
-
dispatch_groups(import)
|
|
584
|
-
end
|
|
585
|
-
|
|
586
|
-
private
|
|
587
|
-
|
|
588
|
-
def dispatch_groups(import)
|
|
589
|
-
# When the import defines a group_column (e.g. ProductTranslations groups
|
|
590
|
-
# by slug) and that field is mapped, group rows by the column value.
|
|
591
|
-
# Otherwise, fall back to slicing pending_and_failed rows into batches of
|
|
592
|
-
# BATCH_SIZE. Either way: pluck the row IDs, set processing_groups_count /
|
|
593
|
-
# completed_groups_count up front so workers can't complete prematurely,
|
|
594
|
-
# then enqueue one ProcessGroupJob per group.
|
|
595
|
-
import.rows.pending_and_failed.in_batches(of: BATCH_SIZE) do |batch|
|
|
596
|
-
ProcessGroupJob.perform_later(import.id, batch.ids)
|
|
597
|
-
end
|
|
598
|
-
end
|
|
599
|
-
end
|
|
600
|
-
end
|
|
601
|
-
end
|
|
602
|
-
```
|
|
118
|
+
**A failed row doesn't stop the import.** The good rows are imported; the bad ones are set aside with the reason.
|
|
603
119
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
```ruby
|
|
609
|
-
module Spree
|
|
610
|
-
module Imports
|
|
611
|
-
class ProcessGroupJob < Spree::Imports::BaseJob
|
|
612
|
-
def perform(import_id, row_ids)
|
|
613
|
-
import = Spree::Import.find(import_id)
|
|
614
|
-
rows = import.rows.where(id: row_ids).pending_and_failed
|
|
615
|
-
|
|
616
|
-
# Large imports process with events disabled and use bulk_process!;
|
|
617
|
-
# otherwise each row is processed individually via row.process!.
|
|
618
|
-
rows.each { |row| row.process! }
|
|
619
|
-
|
|
620
|
-
check_import_completion(import)
|
|
621
|
-
end
|
|
622
|
-
|
|
623
|
-
private
|
|
624
|
-
|
|
625
|
-
# Increments completed_groups_count, then completes the import only once the
|
|
626
|
-
# last group has finished and no rows are still in flight.
|
|
627
|
-
def check_import_completion(import)
|
|
628
|
-
# completed_groups_count += 1
|
|
629
|
-
import.complete! if import.completed_groups_count >= import.processing_groups_count &&
|
|
630
|
-
import.rows.in_flight.none?
|
|
631
|
-
end
|
|
632
|
-
end
|
|
633
|
-
end
|
|
634
|
-
end
|
|
635
|
-
```
|
|
120
|
+
```typescript Admin SDK
|
|
121
|
+
const { data: failures } = await adminClient.imports.rows.list(importJob.id, {
|
|
122
|
+
filter: { status_eq: 'failed' },
|
|
123
|
+
})
|
|
636
124
|
|
|
637
|
-
|
|
125
|
+
failures.forEach((row) => {
|
|
126
|
+
row.row_number // 47
|
|
127
|
+
row.validation_errors // "Price is not a number"
|
|
128
|
+
})
|
|
638
129
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
```ruby
|
|
642
|
-
module Spree
|
|
643
|
-
module Exports
|
|
644
|
-
class GenerateJob < Spree::BaseJob
|
|
645
|
-
queue_as Spree.queues.exports
|
|
646
|
-
|
|
647
|
-
def perform(export_id)
|
|
648
|
-
export = Spree::Export.find_by_prefix_id!(export_id)
|
|
649
|
-
export.generate
|
|
650
|
-
end
|
|
651
|
-
end
|
|
652
|
-
end
|
|
653
|
-
end
|
|
130
|
+
// After fixing the source data
|
|
131
|
+
await adminClient.imports.retryFailedRows(importJob.id)
|
|
654
132
|
```
|
|
655
133
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
## Events
|
|
659
|
-
|
|
660
|
-
Import and export lifecycle events such as [`import.completed` and `export.created`](../../api-reference/webhooks-events.md) can be delivered as webhooks to react when an asynchronous import or export finishes.
|
|
661
|
-
|
|
662
|
-
### Import Events
|
|
663
|
-
|
|
664
|
-
| Event | Trigger |
|
|
665
|
-
|-------|---------|
|
|
666
|
-
| `import.created` | Import record created |
|
|
667
|
-
| `import.completed` | All rows processed |
|
|
668
|
-
|
|
669
|
-
### Export Events
|
|
670
|
-
|
|
671
|
-
| Event | Trigger |
|
|
672
|
-
|-------|---------|
|
|
673
|
-
| `export.created` | Export record created (triggers generation) |
|
|
134
|
+
Retrying re-runs only the failures, so a file where three rows out of ten thousand were wrong doesn't need re-importing whole.
|
|
674
135
|
|
|
675
|
-
|
|
136
|
+
> **WARNING:** A value that isn't a number is rejected, never guessed at. A price written as `"12,50"` fails rather than being read as `1250` — which would be a hundredfold overcharge. Fix the file rather than hoping the import is lenient.
|
|
676
137
|
|
|
677
|
-
|
|
678
|
-
|-------|---------|
|
|
679
|
-
| `import_row.completed` | Row processed successfully |
|
|
680
|
-
| `import_row.failed` | Row processing failed |
|
|
138
|
+
## Interrupted work resumes
|
|
681
139
|
|
|
682
|
-
|
|
140
|
+
Imports remember which row they reached. If the server restarts halfway through a large file, processing picks up where it stopped rather than starting again or skipping the remainder.
|
|
683
141
|
|
|
684
|
-
##
|
|
142
|
+
## Products with several variants
|
|
685
143
|
|
|
686
|
-
|
|
144
|
+
A product with variants spans several rows — one per variant, repeating the product columns. Rows are grouped by the product identifier, so a t-shirt in three sizes is three rows and becomes one product.
|
|
687
145
|
|
|
688
|
-
|
|
689
|
-
# Configure job queues
|
|
690
|
-
Spree.queues.imports = :imports
|
|
691
|
-
Spree.queues.exports = :exports
|
|
692
|
-
```
|
|
146
|
+
Custom fields can be imported too: a column matching a [custom field](metafields.md) you've defined maps to it like any built-in field.
|
|
693
147
|
|
|
694
|
-
|
|
148
|
+
## Knowing when it's finished
|
|
695
149
|
|
|
696
|
-
Imports
|
|
150
|
+
Imports and exports emit [events](events.md) as they progress, which also reach [webhooks](webhooks.md). That's how you trigger the next step of a pipeline — notify a channel, kick off a reindex — without polling.
|
|
697
151
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
## Key Files Reference
|
|
705
|
-
|
|
706
|
-
| File | Purpose |
|
|
707
|
-
|------|---------|
|
|
708
|
-
| `core/app/models/spree/import.rb` | Base import model |
|
|
709
|
-
| `core/app/models/spree/export.rb` | Base export model |
|
|
710
|
-
| `core/app/models/spree/import_schema.rb` | Base schema class |
|
|
711
|
-
| `core/app/models/spree/import_mapping.rb` | Field mapping model |
|
|
712
|
-
| `core/app/models/spree/import_row.rb` | Row model with processing |
|
|
713
|
-
| `core/app/models/spree/imports/products.rb` | Products import type |
|
|
714
|
-
| `core/app/models/spree/import_schemas/products.rb` | Products schema |
|
|
715
|
-
| `core/app/services/spree/imports/row_processors/base.rb` | Base processor |
|
|
716
|
-
| `core/app/services/spree/imports/row_processors/product_variant.rb` | Products processor |
|
|
717
|
-
| `core/app/models/spree/exports/products.rb` | Products export |
|
|
718
|
-
| `core/app/models/spree/exports/orders.rb` | Orders export |
|
|
719
|
-
| `core/app/models/spree/exports/customers.rb` | Customers export |
|
|
720
|
-
| `core/app/jobs/spree/imports/create_rows_job.rb` | Row creation job |
|
|
721
|
-
| `core/app/jobs/spree/imports/process_rows_job.rb` | Row processing job |
|
|
722
|
-
| `core/app/jobs/spree/exports/generate_job.rb` | Export generation job |
|
|
723
|
-
| `admin/app/controllers/spree/admin/imports_controller.rb` | Admin imports controller |
|
|
724
|
-
| `admin/app/controllers/spree/admin/exports_controller.rb` | Admin exports controller |
|
|
152
|
+
| Event | When |
|
|
153
|
+
|---|---|
|
|
154
|
+
| `import.completed` | Every row has been attempted |
|
|
155
|
+
| `import.failed` | The import could not run |
|
|
156
|
+
| `export.completed` | The file is ready to download |
|
|
725
157
|
|
|
726
158
|
## Permissions
|
|
727
159
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
```ruby
|
|
731
|
-
# Allow admin to manage imports/exports
|
|
732
|
-
can :manage, Spree::Import
|
|
733
|
-
can :manage, Spree::Export
|
|
734
|
-
```
|
|
735
|
-
|
|
736
|
-
Records are filtered by `current_ability` ensuring users only export data they have access to.
|
|
160
|
+
Import and export are gated by the same [permissions](staff-roles.md) as the data they touch. Someone who can't see customers can't export them — otherwise export would be a way around the whole permission system.
|
|
737
161
|
|
|
738
|
-
## Related
|
|
162
|
+
## Related
|
|
739
163
|
|
|
740
|
-
- [
|
|
741
|
-
- [
|
|
164
|
+
- [Products](products.md) — the most-imported data
|
|
165
|
+
- [Custom Fields](metafields.md) — importing your own fields
|
|
166
|
+
- [Events](events.md) — reacting to completion
|
|
167
|
+
- [Staff & Roles](staff-roles.md) — who may import and export
|