@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,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
- Spree provides a comprehensive bulk data import and export system for managing large datasets. The system supports CSV file processing with configurable field mapping, asynchronous processing via background jobs, and real-time progress tracking in the admin interface.
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
- ### Import/Export System Diagram
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
- erDiagram
13
- Import {
14
- string number
15
- string type
16
- string status
17
- bigint owner_id
18
- string owner_type
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
- ## Architecture
22
+ ## What can be imported and exported
62
23
 
63
- The import/export system uses several design patterns:
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
- 1. **Single Table Inheritance (STI)**: Import and Export types inherit from base classes
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
- #### Import Schema
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
- ```ruby
294
- module Spree
295
- class ImportSchema
296
- FIELDS = []
41
+ ```typescript Admin SDK
42
+ const exportJob = await adminClient.exports.create({
43
+ type: 'Spree::Exports::Products',
44
+ })
297
45
 
298
- def fields
299
- self.class::FIELDS
300
- end
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
- #### Import Row
337
-
338
- Represents a single CSV row:
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
- ### Creating a Custom Importer
58
+ ### Exporting a filtered set
395
59
 
396
- **Step 1: Create the Import Class**
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
- ```ruby app/models/spree/imports/subscriptions.rb
399
- module Spree
400
- module Imports
401
- class Subscriptions < Spree::Import
402
- def row_processor_class
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
- **Step 2: Define the Schema**
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
- **Step 3: Create the Row Processor**
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
- **Step 4: Register the Import Type**
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
- ```ruby config/initializers/spree.rb
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
- **Step 5: Add Translations**
77
+ ```typescript Admin SDK
78
+ const importJob = await adminClient.imports.create({
79
+ type: 'Spree::Imports::Products',
80
+ file: uploadedFileId,
81
+ })
82
+ ```
490
83
 
491
- ```yaml config/locales/en.yml
492
- en:
493
- spree:
494
- subscriptions: Subscriptions
495
- ```
84
+ Spree reads the header row and tells you which columns it found.
496
85
 
497
- ### Products Import Schema
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
- ### Metafield Support
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
- Both imports and exports support metafields dynamically:
98
+ Obvious matches are suggested for you; you only correct the ones that differ.
542
99
 
543
- **Export:** Metafield definitions are automatically added as CSV columns using the format `metafield.{namespace}.{key}`.
100
+ **Step 3: Watch it run**
544
101
 
545
- **Import:** Map CSV columns to metafield definitions. The system automatically detects columns matching the metafield pattern and updates the corresponding metafield values.
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
- ### CreateRowsJob
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
- Parses CSV and creates ImportRow records:
114
+ ## When rows fail
554
115
 
555
- All import jobs inherit from `Spree::Imports::BaseJob`, which sets `queue_as Spree.queues.imports` once and is shared across the pipeline.
116
+ Some rows will fail. A price with a currency symbol in it, a required field left blank, a duplicate SKU.
556
117
 
557
- ```ruby
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
- ### ProcessGroupJob
605
-
606
- Processes one group of rows and reports completion:
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
- ### GenerateJob (Exports)
125
+ failures.forEach((row) => {
126
+ row.row_number // 47
127
+ row.validation_errors // "Price is not a number"
128
+ })
638
129
 
639
- Generates CSV files for exports:
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
- ### Import Row Events
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
- | Event | Trigger |
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
- ## Configuration
142
+ ## Products with several variants
685
143
 
686
- ### Queue Configuration
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
- ```ruby config/initializers/spree.rb
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
- ### Preferences
148
+ ## Knowing when it's finished
695
149
 
696
- Imports support configurable delimiter:
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
- ```ruby
699
- import.preferred_delimiter = ';' # Default: ','
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
- Access is controlled via CanCanCan:
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 Documentation
162
+ ## Related
739
163
 
740
- - [Admin imports/exports endpoints](../../api-reference/admin-api/endpoints.md) — REST routes and required scopes to trigger imports and exports programmatically.
741
- - [Admin SDK resources](../sdk/admin/resources.md) — typed TypeScript client for driving these back-office operations.
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