spree-shipstation 5.0.0 → 6.0.0

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -1
  3. data/CHANGELOG.md +126 -0
  4. data/CODE_OF_CONDUCT.md +132 -0
  5. data/Gemfile +10 -4
  6. data/README.md +125 -28
  7. data/Rakefile +10 -9
  8. data/app/controllers/spree/shipstation_controller.rb +99 -16
  9. data/app/helpers/spree/shipstation/export_helper.rb +21 -10
  10. data/app/models/spree/integrations/shipstation.rb +64 -25
  11. data/app/presenters/spree/shipstation/export/item_presenter.rb +54 -10
  12. data/app/presenters/spree/shipstation/export/order_presenter.rb +87 -17
  13. data/app/presenters/spree/shipstation/export/weight.rb +20 -1
  14. data/app/views/spree/shipstation/export.xml.builder +9 -7
  15. data/config/initializers/spree.rb +5 -1
  16. data/config/locales/en.yml +4 -5
  17. data/config/routes.rb +12 -2
  18. data/lib/spree/shipstation/engine.rb +4 -13
  19. data/lib/spree/shipstation/errors.rb +52 -2
  20. data/lib/spree/shipstation/export_query.rb +136 -0
  21. data/lib/spree/shipstation/shipment_notice.rb +179 -28
  22. data/lib/spree/shipstation/testing_support/factories/shipstation_integration.rb +2 -0
  23. data/lib/spree/shipstation/version.rb +8 -2
  24. data/lib/spree/shipstation.rb +1 -1
  25. data/spree-shipstation.gemspec +8 -7
  26. metadata +11 -41
  27. data/.github/stale.yml +0 -17
  28. data/.github/workflows/lint.yml +0 -24
  29. data/.github/workflows/security.yml +0 -41
  30. data/.github/workflows/test.yml +0 -32
  31. data/Appraisals +0 -20
  32. data/CLAUDE.md +0 -95
  33. data/app/assets/config/spree_shipstation_manifest.js +0 -1
  34. data/app/models/spree/shipment_decorator.rb +0 -28
  35. data/app/views/spree/admin/integrations/forms/_shipstation.html.erb +0 -6
  36. data/gemfiles/spree_5_2.gemfile +0 -18
  37. data/gemfiles/spree_5_3.gemfile +0 -18
  38. data/gemfiles/spree_5_4.gemfile +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30a88f9939731dc679980577dc0b23a91fdbda25c4019063b3f4728dfb2fe5a9
4
- data.tar.gz: d410ae3a207dd0051ab938c3890c60c515b403723ac43392ede48ab872352ca1
3
+ metadata.gz: 1ec0ccf29f471fcbd60126248195ed5adbb10f9ae5cc5301b23b640308ce6f0f
4
+ data.tar.gz: 97d83306702cbbac6b67f7d4e2bca35676063e397e7e13717ad4eaed7e0d3487
5
5
  SHA512:
6
- metadata.gz: f1d456fbe2b2e4a682b7cc2b62e1c07faad779b449bd8f2da72e5a2ce67b9f133b8cb23a214bb62dfd2ba4d1429a6b1b6fc8839597feb3e445a31fd419de5869
7
- data.tar.gz: ce40b12b11650ef36d848a709a4a0bd513c654a9ca6da8659728e178dd82cdba573504dbaf7ce981ef67509b986d0a11f47157f21280894dec8c90f2fae56769
6
+ metadata.gz: 80e0b2aa61bcc31c1a59249d02522b548899cdea0fa5a7dfb04ba03aa24916105c54434495095f598a54a5d8aa472f58be1cfd2a9eb8c48636efcc8f759eaa84
7
+ data.tar.gz: c3650585bc19e8dd5d89571771b413fa64de828a9f82afd72a899cbb0f751d576367dc8de94bd5d2f18b8e267da6ea84b37cd32a069aa1b12bed3999f530a26f
data/.gitignore CHANGED
@@ -19,5 +19,4 @@ spec/dummy
19
19
  public/spree
20
20
  .ruby-version
21
21
  .ruby-gemset
22
- gemfiles/*.gemfile.lock
23
22
  *.gem
data/CHANGELOG.md CHANGED
@@ -2,6 +2,132 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## 6.0.0
6
+
7
+ Spree 6 support. The major version tracks Spree's, so `spree-shipstation` 6.x
8
+ supports Spree 6, and Spree 5 stays on the 5.0.x releases (the `5-0-stable`
9
+ branch).
10
+
11
+ **Nothing ShipStation or the merchant has configured has to change** for the
12
+ default store. The `/shipstation` URL, the credentials, the XML, the `ready`
13
+ status and the `OrderNumber` of every fulfillment ShipStation already holds stay
14
+ as they were. A store other than the default moves to its integration's URL
15
+ (below).
16
+
17
+ **Run `bin/rake spree:upgrade` right after `db:migrate`, before ShipStation next
18
+ polls.** Spree's upgrade tasks move 5.x shipment statuses to fulfillment statuses
19
+ (`ready` and `pending` become `unfulfilled`, `shipped` becomes `fulfilled`),
20
+ backfill the delivery-method providers the pickup rule reads, recompute the
21
+ statuses of orders whose shipments were pending, ready or shipped, encrypt stored
22
+ secrets, and carry `auto_capture_on_dispatch` over to the store's
23
+ `capture_method`. Until then the export is empty. See "Upgrading
24
+ from 5.0" in the README.
25
+
26
+ **Requirements:** Spree `~> 6.0`, Ruby 3.3 or newer, pagy 43.6 or newer. The 5.x
27
+ releases do not install on Spree 6.
28
+
29
+ ### Changed
30
+
31
+ - **Orders are exported per fulfillment**, since Spree 6 replaced shipments with
32
+ fulfillments. `OrderID` is the fulfillment's id and `OrderNumber` its number:
33
+ the number a fulfillment already had before Spree 6, or `R1001-F1` style
34
+ (order number, `-F`, position) for new ones. Totals stay order-level, as before.
35
+ - **Only fulfillments Spree will let ship are exported**: placed, still
36
+ unfulfilled, paid, with nothing backordered, and not a pickup. "Paid" means
37
+ paid, overcharged or partially refunded; or nothing owed; or, on a store whose
38
+ capture method is `on_dispatch` or `manual`, authorized, or partially paid with
39
+ the rest authorized (mixed tender, such as store credit plus a card). As before,
40
+ unpaid or backordered fulfillments do not appear in ShipStation until they can
41
+ ship: 5.x exported only `ready` shipments. Results are ordered by last update,
42
+ oldest first.
43
+ - **Ship notices fulfil through Spree 6's fulfil workflow.** When Spree refuses
44
+ (an unpaid order, backordered items, a canceled fulfillment) the notice is
45
+ answered with `400` and Spree's reason, and nothing is recorded. It is never
46
+ answered `200` for a fulfillment Spree did not ship.
47
+ - **Ship notices are retry-safe.** A notice for a fulfillment that is already
48
+ fulfilled or delivered only records or corrects the tracking number: it does
49
+ not fulfil the parcel or publish the event again. A retry that names a tracking
50
+ number already on another delivery of the same fulfillment is refused with
51
+ `400` ("Tracking number has already been taken"). Concurrent retries are
52
+ serialized under the order's row lock. A parcel still marked `shipped` from 5.x,
53
+ before Spree's upgrade task renames the status, is treated the same way.
54
+ - **A notice without an order number is refused** with `400`. New fulfillments
55
+ store no number, so a blank one would otherwise match any of them.
56
+ - **Capture on dispatch follows Spree 6's `capture_method`**, set on the store or
57
+ overridden per payment method. A declined capture answers `400` ("Could not
58
+ capture payment: ...") and keeps nothing: the fulfillment stays unfulfilled,
59
+ the payment stays pending and the tracking written during the attempt is rolled
60
+ back, so the retry starts from the same place.
61
+ - **Shipped notifications are the host app's to send.** Spree 6 core sends no
62
+ shipped email itself. The fulfil workflow publishes `fulfillment.fulfilled` with
63
+ `notify_customer` set, and your application's subscriber decides whether, and
64
+ how, to tell the customer. On a declined capture core publishes that event
65
+ inside the transaction that is then rolled back, so a subscriber that notifies
66
+ at once can announce a shipment that did not happen (Spree's own fulfil endpoint
67
+ behaves the same).
68
+ - `ImageUrl` uses the media's still image, so a video thumbnail exports its
69
+ poster. It is still a root-relative path.
70
+ - The export returns **50 fulfillments per page**, as documented. (pagy 43, which
71
+ Spree 6 requires, ignores the option the gem used to set this and would have
72
+ given 25.) A `page` that is not a number is read as page 1.
73
+ - The integration appears in Spree 6's dashboard under the shipping group, with a
74
+ description, logo and link to the README. Its form is generated from the
75
+ username and password preferences. Spree 6's migration copies the password into
76
+ `secret_preferences` as plain text, and `bin/rake spree:upgrade` encrypts it
77
+ when Active Record encryption keys are configured.
78
+ - Both endpoints now run on a plain `ActionController::Base` controller, since
79
+ Spree 6 removed the base controller and helper the 5.x one used.
80
+
81
+ ### Added
82
+
83
+ - **`/shipstation/<integration id>`** (`GET` and `POST`, with or without `.xml`)
84
+ serves the store of the active ShipStation integration with that `int_` prefixed
85
+ id. Spree 6 no longer picks the store from the request's host, so a multi-store
86
+ host points each store's ShipStation at its own integration's URL. The bare
87
+ `/shipstation` path is unchanged and belongs to the default store.
88
+ - **Ship notices are matched on ShipStation's `OrderID`** when the notice's XML
89
+ body carries it. A new fulfillment's number is its place in its order, so when
90
+ Spree deletes a parcel whose items were all moved out, the parcels after it
91
+ take new numbers; the `OrderID` (the fulfillment's id) does not change. A notice
92
+ whose `OrderID` belongs to another order is refused with `400` ("Shipment ...
93
+ does not match order id ..."), and one for a parcel Spree has deleted since
94
+ ShipStation imported it is refused with `400` instead of fulfilling the sibling
95
+ that now has its number; re-import the order from ShipStation. A notice without
96
+ an `OrderID` is matched on its number.
97
+ - **The `carrier` ShipStation sends** is recorded on the delivery when it matches
98
+ one of Spree's `tracking_carriers` (for example `UPS` or `Royal Mail`). Otherwise
99
+ Spree detects the carrier from the tracking number.
100
+ - `GET /shipstation` without the `.xml` extension answers with XML.
101
+ - An integration whose stored username or password is blank refuses every request.
102
+
103
+ ### Removed
104
+
105
+ - The gem's own payment-capture code and its use of **`auto_capture_on_dispatch`**.
106
+ Spree 6's `capture_method` replaces that setting, and Spree's upgrade task
107
+ carries it over to the store's `capture_method`.
108
+ - The `Spree::Shipment` decorator and its `exportable` and `between` scopes. The
109
+ gem no longer patches core models; `Spree::Shipstation::ExportQuery` selects what
110
+ is exported.
111
+ - The ERB admin form, the asset manifest and the `spree_extension` dependency.
112
+
113
+ ### Internal
114
+
115
+ - Specs run against the real routes and Spree's real fulfil workflow, with only
116
+ ShipStation's side simulated. The suite runs with no deprecation warnings.
117
+ - CI tests the oldest and newest Spree 6 the gem allows (6.0.0 and the latest
118
+ release) on Ruby 3.3 and 4.0, weekly as well as on pushes.
119
+
120
+ ## 5.0.1
121
+
122
+ ### Fixed
123
+
124
+ - Export the correct `<Quantity>` for lines ordered more than once. Spree 5 stores
125
+ a quantity on each `Spree::InventoryUnit` rather than creating one unit row per
126
+ item, so counting rows reported every line as a quantity of 1 and ShipStation
127
+ under-picked multi-quantity orders. The quantity is now summed across the
128
+ line's inventory units, which also keeps a line split across shipments
129
+ reporting only the units in its own shipment.
130
+
5
131
  ## 5.0.0
6
132
 
7
133
  First public release on RubyGems, as `spree-shipstation`.
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ m.kennedy@me.com.
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/Gemfile CHANGED
@@ -1,14 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
- gem "appraisal"
4
5
  gem "benchmark"
5
6
  gem "brakeman", require: false
6
7
  gem "bundler-audit", require: false
7
- gem "propshaft"
8
- gem "rails-controller-testing"
9
8
  gem "rspec-xsd"
10
- gem "spree_dev_tools"
9
+ # Constrained here because the gemspec no longer declares it; Bundler 2 would
10
+ # otherwise resolve 0.6.3, which is broken on Spree 6.
11
+ gem "spree_dev_tools", "~> 1.0"
11
12
  gem "standard"
12
13
  gem "sqlite3"
13
14
 
15
+ # CI runs a matrix of Spree versions. SPREE_VERSION pins one (e.g. "6.0.0");
16
+ # left unset, Bundler resolves the newest release the gemspec allows.
17
+ spree_version = ENV.fetch("SPREE_VERSION", "")
18
+ gem "spree", spree_version unless spree_version.empty?
19
+
14
20
  gemspec
data/README.md CHANGED
@@ -3,15 +3,25 @@
3
3
  ![CI](https://github.com/aypex-io/spree-shipstation/workflows/CI/badge.svg)
4
4
  ![Standard Rb](https://github.com/aypex-io/spree-shipstation/workflows/Standard%20Rb/badge.svg)
5
5
 
6
- The spree-shipstation integration connects your Spree stores with [ShipStation](https://www.shipstation.com), allowing ShipStation to pull shipments from your store, and when a shipment is sent, update the order with a tracking number and mark it as shipped.
6
+ The spree-shipstation integration connects your Spree stores with [ShipStation](https://www.shipstation.com), allowing ShipStation to pull the fulfillments that are ready to ship from your store, and when one is sent, update the order with a tracking number and mark it as fulfilled.
7
7
 
8
+ ## Requirements
9
+
10
+ - Spree 6 (`~> 6.0`)
11
+ - Ruby 3.3 or newer
12
+
13
+ Spree 5 stores stay on the 5.0.x releases, which live on the [`5-0-stable`](https://github.com/aypex-io/spree-shipstation/tree/5-0-stable) branch:
14
+
15
+ ```ruby
16
+ gem "spree-shipstation", "~> 5.0"
17
+ ```
8
18
 
9
19
  ## Installation
10
20
 
11
21
  1. Add this extension to your Gemfile with this line:
12
22
 
13
23
  ```ruby
14
- gem "spree-shipstation"
24
+ gem "spree-shipstation", "~> 6.0"
15
25
  ```
16
26
 
17
27
  2. Install the gem using Bundler
@@ -24,7 +34,7 @@ The extension registers itself with Spree's integration framework automatically
24
34
 
25
35
  ### Step 1: Configuring Spree
26
36
 
27
- Visit the **Integrations** section of your Spree store and configure the ShipStation integration by creating a unique username and password.
37
+ Visit the **Integrations** section of your Spree dashboard and configure the ShipStation integration (listed with the shipping integrations) by creating a unique username and password. The username is 10 to 30 characters of letters, numbers and `. _ @ + -`; the password is 20 to 60 characters and needs an uppercase letter, a number and a special character, one of `! @ # $ % ^ & * ( ) , . ? " : { } | < >`. Other characters such as `-`, `_`, `+` and `=` may appear but do not count as the special character.
28
38
 
29
39
  ### Step 2: Configuring ShipStation
30
40
 
@@ -34,41 +44,117 @@ Enter the following details:
34
44
 
35
45
  - **Username**: The username you created in Step 1.
36
46
  - **Password**: The password you created in Step 1.
37
- - **URL to custom page**: `https://your-store-domain.com/shipstation.xml`.
47
+ - **URL to custom page**: `https://shop.example.com/shipstation.xml`.
38
48
 
39
- There are five shipment states for an order (= shipment) in ShipStation. These states do not
40
- necessarily align with Spree, but you can configure ShipStation to create a mapping for your
41
- specific needs. Here's the default mapping:
49
+ That URL belongs to your **default store**. Any other store (a multi-store setup) must use the URL that names its ShipStation integration:
42
50
 
43
- ShipStation description | ShipStation status | Spree status
44
- ------------------------|--------------------|---------------
45
- Awaiting Payment | `unpaid` | `pending`
51
+ ```
52
+ https://shop.example.com/shipstation/int_abc123.xml
53
+ ```
54
+
55
+ `int_abc123` stands for the integration's prefixed ID, which starts with `int_`. In a Rails console, `Spree::Integrations::Shipstation.all.map(&:prefixed_id)` lists them. The credentials are always the ones set on that integration. The `.xml` is optional on both URLs.
56
+
57
+ ### Order status
58
+
59
+ Every order the gem sends has the status `ready`, so in ShipStation's Custom Store settings map `ready` to **Awaiting Shipment**. ShipStation's other statuses are never sent: the gem exports only fulfillments that can ship now, and ShipStation marks one as shipped itself once the label exists.
60
+
61
+ ShipStation description | ShipStation status | Sent by the gem
62
+ ------------------------|--------------------|----------------
46
63
  Awaiting Shipment | `paid` | `ready`
47
- Shipped | `shipped` | `shipped`
48
- Cancelled | `cancelled` | `cancelled`
49
- On-Hold | `on-hold` | `pending`
64
+ Anything else | — | never; only `ready` is exported
65
+
66
+ ## What ShipStation pulls
67
+
68
+ Spree 6 splits an order into fulfillments (parcels), and the gem exports one ShipStation order per fulfillment. An order with several fulfillments therefore appears once for each, every copy carrying the order's totals.
69
+
70
+ A fulfillment is exported when all of these hold:
71
+
72
+ - it belongs to the store and is still **unfulfilled**;
73
+ - its order is **placed**, so drafts and canceled orders are left out;
74
+ - its order is **paid**: paid, overcharged or partially refunded; or it owes nothing (a total of zero); or, on a store whose capture method is `on_dispatch` or `manual`, it is only authorized, or partially paid with the rest authorized (mixed tender, such as store credit plus a card) (see [Payment capture](#payment-capture));
75
+ - **none of its items is backordered**;
76
+ - it is **not a pickup**: fulfillments collected in person (pickup or pickup point delivery methods) are not shipped.
77
+
78
+ Apart from the pickup rule, these are most of the checks Spree makes again when the parcel is fulfilled, so ShipStation is rarely offered a parcel Spree would refuse. When it is fulfilled, Spree also checks that the stock location has the units on hand, and the limits under [Payment capture](#payment-capture) apply. A parcel refused then gets a `400` with Spree's reason.
79
+
80
+ ShipStation's `start_date` and `end_date` (`MM/DD/YYYY HH:MM`, UTC) limit the export to fulfillments whose own or whose order's last update falls in that window. A date the gem cannot read is ignored. Results are ordered by last update, oldest first.
81
+
82
+ `OrderID` is the fulfillment's id and `OrderNumber` its number. Fulfillments created on Spree 6 are numbered after their order, for example `R1001-F1`; fulfillments created before 6.0 keep the number they already had, so what ShipStation already holds does not change.
83
+
84
+ ## What happens on a ship notice
85
+
86
+ When a label is created, ShipStation posts to the same URL with `order_number` (the `OrderNumber` it was given), `tracking_number` and `carrier` in the query string, and the notice again as an XML body that carries the `OrderID` it was given. The gem finds that fulfillment in the store, by its `OrderID` when the body carries one and by its number otherwise, and:
87
+
88
+ - **fulfils it** through Spree's own fulfil workflow, recording the tracking number, and answers `200`;
89
+ - records the carrier when ShipStation's carrier name matches one of Spree's `tracking_carriers` (for example `UPS`, `USPS`, `FedEx` or `Royal Mail`); otherwise Spree detects it from the tracking number;
90
+ - if the fulfillment is **already fulfilled or delivered**, only records or corrects the tracking number and answers `200`. ShipStation retries, and a retry does not fulfil the parcel or publish the event again. The one exception is a tracking number that is already on a different delivery of the same fulfillment, which is refused (below);
91
+ - otherwise answers `400` with a plain-text reason and changes nothing. ShipStation shows the reason to the warehouse. The reasons include:
92
+ - `Could not find shipment with number …`, for an unknown or blank number, or an `OrderID` whose fulfillment no longer exists;
93
+ - `Shipment … does not match order id …`, when the `OrderID` belongs to a fulfillment of another order;
94
+ - `Tracking number is required`;
95
+ - `Shipment … is canceled`;
96
+ - Spree's own message when it refuses to fulfil, for example `This order is not paid for yet`, or its message about backordered units;
97
+ - `Could not capture payment: …`, when the gateway declines a capture on dispatch;
98
+ - `Tracking number has already been taken`, when a retry for a fulfilled parcel names a tracking number that already belongs to another delivery of the same fulfillment.
99
+
100
+ The gem never forces a fulfilment past Spree's paid and backordered checks, and never splits a fulfillment.
101
+
102
+ ### Why the `OrderID` comes first
103
+
104
+ A new fulfillment's number is its place in its order: `R1001-F2` is the second parcel. When every item is moved out of a parcel (with the dashboard's split or change-location actions, say), Spree deletes it, and the parcels after it move up a place and take new numbers. A fulfillment's id never changes, so the gem trusts the `OrderID` over the number. A notice for a renumbered parcel still fulfils the right one, and a notice for a parcel Spree deleted after ShipStation imported it is refused with a `400` rather than fulfilling a sibling that now has its number. Re-import the order from ShipStation to pick up its parcels as they now are.
105
+
106
+ So, in an order with several parcels, avoid moving every item out of a parcel ShipStation has already imported: that parcel is no longer valid in ShipStation.
50
107
 
51
108
  ## Configuration
52
109
 
53
- ### Payment capture on dispatch
110
+ ### Payment capture
111
+
112
+ The gem has no capture setting of its own. Capture on dispatch follows Spree 6's `capture_method`, set on the store and optionally overridden by each payment method:
113
+
114
+ - `checkout` (the default) charges as the order is placed. Authorized orders are not yet paid, so they are not exported.
115
+ - `on_dispatch` authorizes at checkout and charges when the fulfillment is fulfilled. The gem exports authorized orders, and partially paid ones whose rest is authorized, and Spree captures the payment as the ship notice is applied. If the gateway declines, ShipStation gets a `400` ("Could not capture payment: …") and nothing is kept: the fulfillment stays unfulfilled, the payment stays pending and any tracking number written during the attempt is rolled back. The next retry, or the merchant's fix to the payment, fulfils it.
116
+ - `manual` authorizes at checkout and leaves charging to your staff. The gem exports authorized and partially paid orders, and Spree lets them ship without charging them.
117
+
118
+ Two limits:
54
119
 
55
- The integration respects Spree's `auto_capture_on_dispatch` setting. When enabled in your Spree store, pending payments are captured automatically before a shipment is marked as shipped. If a payment capture fails, an error is returned to ShipStation (HTTP 400), preventing the shipment from being marked as shipped until the issue is resolved.
120
+ - Whether an authorized or partially paid order is exported depends on the **store's** capture method only. A store that captures at checkout but has a payment method that overrides it to `on_dispatch` never exports that method's authorized orders, and does so silently. To export them, set the store's capture method to `on_dispatch` or `manual`, bearing in mind that this changes it for every payment method that has no setting of its own. In the reverse case, a store set to `on_dispatch` with a payment method overridden to capture at checkout, an authorized order is exported and the ship notice is then refused with Spree's reason.
121
+ - A partially refunded order is exported, because the rest of it may still ship, but Spree compares the net captured amount (after refunds) with what the order was due, so the ship notice may still be refused with Spree's reason. Likewise a partially paid order ships only while the rest is authorized on payment methods that defer capture.
56
122
 
57
- Payment capture happens **synchronously** within the shipnotify webhook request (inside a database transaction) so that a shipment is never marked as shipped against an uncaptured payment. ShipStation automatically retries failed webhook deliveries, and the operation is designed to be safe to repeat: a shipment that is already `shipped` is not shipped again, and only still-pending payments are captured. If your payment gateway is slow, be aware that the capture round-trip occurs in the request cycle.
123
+ ### Fulfillment providers
124
+
125
+ Give the delivery methods whose parcels ShipStation ships Spree's **Manual** fulfillment provider. On a ship notice, Spree's fulfil workflow also buys a label and tells the delivery method's provider that the parcel has shipped; for Manual, both do nothing. A provider that buys labels or hands parcels to a 3PL would act a second time for a parcel ShipStation has already shipped. A declined capture on dispatch does not undo those calls either, even though everything Spree wrote is rolled back.
126
+
127
+ ### Telling the customer
128
+
129
+ Spree core does not send a shipped email by itself (as of Spree 6.0.1). When a fulfillment is fulfilled, Spree publishes the `fulfillment.fulfilled` event, and the gem asks for the customer to be notified (`notify_customer: true`). Your application decides whether, and how, to tell the customer, by subscribing to `fulfillment.fulfilled`. Without a subscriber, no shipped email goes out.
130
+
131
+ One caveat: when a capture on dispatch is declined, Spree publishes `fulfillment.fulfilled` synchronously inside the transaction that is then rolled back. A subscriber that notifies at once, rather than after the transaction commits, can tell a customer their order has shipped when the attempt was declined and nothing was kept. Spree's own fulfil endpoint behaves the same way, so if that matters to you, make your subscriber send only after the transaction has committed.
58
132
 
59
133
  ### Pagination
60
134
 
61
- The export endpoint returns up to **50 shipments per page**. ShipStation handles pagination automatically using the `page` query parameter.
135
+ The export endpoint returns up to **50 fulfillments per page**. ShipStation handles pagination automatically using the `page` query parameter; a `page` that is not a number is read as the first page.
136
+
137
+ ## Upgrading from 5.0
138
+
139
+ - Bump the gem to `~> 6.0` alongside your upgrade to Spree 6.
140
+ - The gem has no data migration of its own. Spree 6's migrations rename shipments to fulfillments, keeping their ids and numbers, and copy the integration's password into `secret_preferences` as plain text. `bin/rake spree:upgrade` (next step) encrypts it when Active Record encryption keys are configured.
141
+ - **Run `bin/rake spree:upgrade` right after `db:migrate`, before ShipStation next polls.** Spree's upgrade tasks move 5.x shipment statuses to fulfillment statuses (`ready` and `pending` become `unfulfilled`, `shipped` becomes `fulfilled`), backfill the delivery-method providers the pickup rule reads, recompute the payment and fulfillment statuses of orders whose shipments were pending, ready or shipped, encrypt stored secrets, and carry settings such as `auto_capture_on_dispatch` over to the store's `capture_method`. An order in another state, such as partly shipped or backordered, keeps its 5.x payment status until Spree next updates it. Until then the export is empty, since no fulfillment is `unfulfilled` yet. Meanwhile a parcel still marked `shipped` is treated as fulfilled: a ship notice only records its tracking.
142
+ - **ShipStation needs no changes** for the default store: same URL, same credentials, same `OrderNumber` for parcels it has already imported, same `ready` status.
143
+ - New fulfillments are numbered like `R1001-F1`. A store other than the default must use `/shipstation/<integration id>` (see Step 2): 5.x picked the store from the request's host, and Spree 6 does not, so the bare URL serves only the default store.
144
+ - Capture on dispatch now follows Spree 6's `capture_method` setting (on the store or the payment method). Spree's upgrade task carries `auto_capture_on_dispatch` over to the store's `capture_method`.
145
+ - As before, unpaid or backordered fulfillments do not appear in ShipStation until they can ship: 5.x exported only `ready` shipments.
146
+ - A ship notice for something Spree will not ship is answered with `400` and Spree's reason.
147
+ - Spree 6 core sends no shipped email on its own, so subscribe to `fulfillment.fulfilled` if your store relied on the 5.x email (see [Telling the customer](#telling-the-customer)).
62
148
 
63
149
  ## Security considerations
64
150
 
65
- - **Serve the endpoints over HTTPS.** Both `/shipstation` endpoints authenticate with HTTP Basic Auth, which transmits the configured username and password (base64-encoded) on every request. Always terminate these requests over TLS in production so the credentials are not exposed in transit. Credentials are compared in constant time to avoid timing attacks.
66
- - **Consider rate limiting.** The gem does not throttle authentication attempts. If you want brute-force protection, add it at the application or edge layer (for example, [`rack-attack`](https://github.com/rack/rack-attack)). The enforced credential length (10–30 character username, 20–60 character complex password) already makes guessing impractical.
67
- - **Response codes.** When no active ShipStation integration is configured, the endpoints respond with `404` before authentication is checked; a configured-but-unauthenticated request responds with `401`. This is intentional, but be aware it reveals whether the integration is configured.
151
+ - **Serve the endpoints over HTTPS.** Both `/shipstation` endpoints authenticate with HTTP Basic Auth, which transmits the configured username and password (base64-encoded) on every request. Always terminate these requests over TLS in production so the credentials are not exposed in transit. Credentials are compared in constant time to avoid timing attacks, and an integration whose stored username or password is blank refuses every request.
152
+ - **Consider rate limiting.** The gem does not throttle authentication attempts. If you want brute-force protection, add it at the application or edge layer (for example, [`rack-attack`](https://github.com/rack/rack-attack) or Rails' `rate_limit`). The enforced credential length (10–30 character username, 20–60 character complex password) already makes guessing impractical.
153
+ - **Response codes.** When no active ShipStation integration is configured, the endpoints respond with `404` before authentication is checked; a configured-but-unauthenticated request responds with `401`. This is intentional, but be aware it reveals whether the integration is configured. An integration that is switched off is the same as none.
68
154
 
69
155
  ## Performance
70
156
 
71
- The export query eager-loads its association graph to avoid N+1 queries. The `exportable` scope filters shipments by `state` and orders them by `updated_at`, joining and filtering orders by their `updated_at`. The gem ships no migrations of its own; on large stores, ensure the relevant Spree core columns (`spree_shipments.state`, `spree_shipments.updated_at`, `spree_orders.updated_at`) are adequately indexed in your application's database.
157
+ The export query eager-loads its association graph to avoid N+1 queries. It selects unfulfilled fulfillments, joins their orders and filters by payment status, and orders and windows by `updated_at`. The gem ships no migrations of its own. Spree 6's schema indexes `spree_fulfillments.status` and `spree_orders.payment_status`, but not `spree_fulfillments.updated_at` or `spree_orders.updated_at`; on large stores, consider indexing those two in your application's database.
72
158
 
73
159
  ## Usage
74
160
 
@@ -76,9 +162,10 @@ There's nothing you need to do. Once properly configured, the integration just w
76
162
 
77
163
  ### Compatibility
78
164
 
79
- This extension works with the following Spree versions:
80
- - 5.x
81
-
165
+ spree-shipstation | Spree | Ruby
166
+ ------------------|-------|------
167
+ 6.0.x | 6.x (`~> 6.0`) | 3.3 or newer
168
+ 5.0.x, on [`5-0-stable`](https://github.com/aypex-io/spree-shipstation/tree/5-0-stable) | 5.x | 3.1 or newer
82
169
 
83
170
  ### Testing
84
171
 
@@ -94,6 +181,13 @@ To run the tests use:
94
181
  bundle exec rake
95
182
  ```
96
183
 
184
+ CI runs the suite against Spree 6.0.0 and the newest 6.x release. To test against a specific Spree version locally:
185
+
186
+ ```shell
187
+ SPREE_VERSION=6.0.0 bundle update spree
188
+ bundle exec rake
189
+ ```
190
+
97
191
  ### Code Formatting
98
192
 
99
193
  To check your code formatting with [Standard Rb](https://github.com/testdouble/standard) run:
@@ -110,11 +204,14 @@ bundle exec standardrb --fix
110
204
 
111
205
  ## Releasing
112
206
 
113
- ```bash
114
- bundle exec gem bump -p -t
115
- bundle exec gem release
116
- ```
207
+ Bump `Spree::Shipstation::VERSION` in `lib/spree/shipstation/version.rb`, add the release to the changelog, and merge. Pushing a `v*` tag publishes the gem to RubyGems through the Release workflow.
117
208
 
118
209
  ## License
119
210
 
120
211
  Copyright (c) 2021-2026 Matthew Kennedy, released under the MIT License.
212
+
213
+ ## Code of Conduct
214
+
215
+ Everyone interacting in this project's codebases, issue trackers, and
216
+ communication channels is expected to follow the [code of
217
+ conduct](CODE_OF_CONDUCT.md).
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler"
2
4
  Bundler::GemHelper.install_tasks
3
5
 
@@ -21,19 +23,18 @@ task :test_app do
21
23
  # literal `require ENV['LIB_NAME']` and templates the same string into the
22
24
  # generated spec/dummy/config/application.rb.
23
25
  ENV["LIB_NAME"] = "spree/shipstation"
24
- Rake::Task["extension:test_app"].execute(
25
- install_admin: true
26
- )
26
+ # Spree 6's extension:test_app takes no install_admin option.
27
+ Rake::Task["extension:test_app"].execute
27
28
  end
28
29
 
29
30
  namespace :dummy do
30
31
  # Spree's test_app task runs `db:migrate` in the generator's own process with
31
- # output redirected to /dev/null and its exit status ignored. On the heavier
32
- # Spree 5.3+/5.4 setup path (admin install + Tailwind/asset build) that step
33
- # can be interrupted mid schema-dump, leaving a TRUNCATED db/schema.rb. Rails
34
- # then loads that broken dump on the next migrate and fails, so the dummy
35
- # database ends up empty and the whole suite errors with
36
- # "Could not find table 'spree_*'".
32
+ # output redirected to /dev/null and its exit status ignored. If a migration
33
+ # fails partway (for example, Spree's preferences conversion eager loads the
34
+ # whole app, so any class that fails to load stops it), the dummy database is
35
+ # left half-built and the whole suite errors with weird failures or
36
+ # "Could not find table 'spree_*'", with no hint of the cause. Rails then
37
+ # loads a possibly truncated db/schema.rb dump on the next migrate too.
37
38
  #
38
39
  # Make setup deterministic: discard any (possibly corrupt) schema dump and
39
40
  # rebuild the database from the migration files, in a fresh process and