spree-shipstation 5.0.0 → 5.0.1
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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +46 -0
- data/CHANGELOG.md +11 -0
- data/CLAUDE.md +3 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/Gemfile +2 -0
- data/README.md +6 -0
- data/Rakefile +2 -0
- data/app/controllers/spree/shipstation_controller.rb +13 -0
- data/app/helpers/spree/shipstation/export_helper.rb +17 -0
- data/app/models/spree/integrations/shipstation.rb +18 -0
- data/app/models/spree/shipment_decorator.rb +4 -0
- data/app/presenters/spree/shipstation/export/item_presenter.rb +43 -4
- data/app/presenters/spree/shipstation/export/order_presenter.rb +59 -3
- data/app/presenters/spree/shipstation/export/weight.rb +20 -1
- data/config/initializers/spree.rb +2 -0
- data/lib/spree/shipstation/engine.rb +3 -0
- data/lib/spree/shipstation/errors.rb +18 -0
- data/lib/spree/shipstation/shipment_notice.rb +32 -2
- data/lib/spree/shipstation/testing_support/factories/shipstation_integration.rb +2 -0
- data/lib/spree/shipstation/version.rb +6 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59253a22e0dcd58163a5f9e3380f1e1c502f8256e05f7b2d65755b01a4879ec8
|
|
4
|
+
data.tar.gz: eb36256ce70cc0ab15858669bf823b4435f2a8d167a0517425af042db83dd95d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4257f6418fa187a10b4f7b178f4b36d69516ba08d65d826260df6ff05056c9e63b176f97d61ae56c3645471f4868db03118f9d608d39eaed26d1d50bcce8dbad
|
|
7
|
+
data.tar.gz: 53537dda148d24860c62affd5b79f041bfce4431956b0a643e48cb3879229957a6ecad3476d5b6ae82b2d6ac6653b04d7f277345d39e0149bcdd5de0017d169f
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
# Publishes to RubyGems.org via Trusted Publishing (OIDC) when a v* tag is
|
|
3
|
+
# pushed. There is no API token or secret involved: GitHub mints a short-lived
|
|
4
|
+
# identity token and RubyGems.org exchanges it for a push-scoped credential.
|
|
5
|
+
#
|
|
6
|
+
# Requires a trusted publisher registered on RubyGems.org for this gem with:
|
|
7
|
+
# owner aypex-io
|
|
8
|
+
# repository spree-shipstation
|
|
9
|
+
# workflow release.yml
|
|
10
|
+
# environment release
|
|
11
|
+
#
|
|
12
|
+
# rubygems/release-gem runs `bundle exec rake release`. Bundler's GemHelper
|
|
13
|
+
# skips tagging when the tag already exists, which it does here because the tag
|
|
14
|
+
# push is what triggered the run.
|
|
15
|
+
name: Release
|
|
16
|
+
|
|
17
|
+
on:
|
|
18
|
+
push:
|
|
19
|
+
tags:
|
|
20
|
+
- 'v*'
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
push:
|
|
24
|
+
name: Push gem to RubyGems.org
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
|
|
27
|
+
permissions:
|
|
28
|
+
contents: write
|
|
29
|
+
id-token: write
|
|
30
|
+
|
|
31
|
+
# Must match the environment registered on the RubyGems trusted publisher.
|
|
32
|
+
environment: release
|
|
33
|
+
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v6
|
|
36
|
+
with:
|
|
37
|
+
# release-gem installs its own git credentials for the release task.
|
|
38
|
+
persist-credentials: false
|
|
39
|
+
|
|
40
|
+
- name: Set up Ruby
|
|
41
|
+
uses: ruby/setup-ruby@v1
|
|
42
|
+
with:
|
|
43
|
+
ruby-version: ruby
|
|
44
|
+
bundler-cache: true
|
|
45
|
+
|
|
46
|
+
- uses: rubygems/release-gem@v1
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 5.0.1
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Export the correct `<Quantity>` for lines ordered more than once. Spree 5 stores
|
|
10
|
+
a quantity on each `Spree::InventoryUnit` rather than creating one unit row per
|
|
11
|
+
item, so counting rows reported every line as a quantity of 1 and ShipStation
|
|
12
|
+
under-picked multi-quantity orders. The quantity is now summed across the
|
|
13
|
+
line's inventory units, which also keeps a line split across shipments
|
|
14
|
+
reporting only the units in its own shipment.
|
|
15
|
+
|
|
5
16
|
## 5.0.0
|
|
6
17
|
|
|
7
18
|
First public release on RubyGems, as `spree-shipstation`.
|
data/CLAUDE.md
CHANGED
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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
data/README.md
CHANGED
|
@@ -118,3 +118,9 @@ bundle exec gem release
|
|
|
118
118
|
## License
|
|
119
119
|
|
|
120
120
|
Copyright (c) 2021-2026 Matthew Kennedy, released under the MIT License.
|
|
121
|
+
|
|
122
|
+
## Code of Conduct
|
|
123
|
+
|
|
124
|
+
Everyone interacting in this project's codebases, issue trackers, and
|
|
125
|
+
communication channels is expected to follow the [code of
|
|
126
|
+
conduct](CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Spree
|
|
4
|
+
##
|
|
5
|
+
# HTTP endpoints ShipStation polls for ready shipments and posts ship notices to.
|
|
6
|
+
#
|
|
4
7
|
class ShipstationController < Spree::BaseController
|
|
5
8
|
include Spree::IntegrationsHelper
|
|
6
9
|
include Pagy::Method
|
|
@@ -10,6 +13,11 @@ module Spree
|
|
|
10
13
|
before_action :ensure_active_integration
|
|
11
14
|
before_action :authenticate_shipstation
|
|
12
15
|
|
|
16
|
+
##
|
|
17
|
+
# XML export of ready shipments for ShipStation to pull.
|
|
18
|
+
#
|
|
19
|
+
# @return [void]
|
|
20
|
+
#
|
|
13
21
|
def export
|
|
14
22
|
@pagy, @shipments = pagy(
|
|
15
23
|
current_store.shipments
|
|
@@ -24,6 +32,11 @@ module Spree
|
|
|
24
32
|
end
|
|
25
33
|
end
|
|
26
34
|
|
|
35
|
+
##
|
|
36
|
+
# Applies a ship notification from ShipStation and marks the shipment shipped.
|
|
37
|
+
#
|
|
38
|
+
# @return [void]
|
|
39
|
+
#
|
|
27
40
|
def shipnotify
|
|
28
41
|
Shipstation::ShipmentNotice.from_payload(params.permit(:order_number, :tracking_number).to_h, store: current_store).apply
|
|
29
42
|
head :ok
|
|
@@ -4,13 +4,30 @@ require "builder"
|
|
|
4
4
|
|
|
5
5
|
module Spree
|
|
6
6
|
module Shipstation
|
|
7
|
+
##
|
|
8
|
+
# Builds ShipStation address XML nodes for the export template.
|
|
9
|
+
#
|
|
7
10
|
module ExportHelper
|
|
8
11
|
DATE_FORMAT = "%m/%d/%Y %H:%M"
|
|
9
12
|
|
|
13
|
+
##
|
|
14
|
+
# Emits a +BillTo+ node without street-level fields.
|
|
15
|
+
#
|
|
16
|
+
# @param xml [Builder::XmlMarkup] the XML builder
|
|
17
|
+
# @param address [Spree::Address, NilClass] billing address
|
|
18
|
+
# @return [String, NilClass]
|
|
19
|
+
#
|
|
10
20
|
def self.bill_address(xml, address)
|
|
11
21
|
render_address(xml, address, "BillTo", include_street: false)
|
|
12
22
|
end
|
|
13
23
|
|
|
24
|
+
##
|
|
25
|
+
# Emits a +ShipTo+ node including street-level fields.
|
|
26
|
+
#
|
|
27
|
+
# @param xml [Builder::XmlMarkup] the XML builder
|
|
28
|
+
# @param address [Spree::Address, NilClass] shipping address
|
|
29
|
+
# @return [String, NilClass]
|
|
30
|
+
#
|
|
14
31
|
def self.ship_address(xml, address)
|
|
15
32
|
render_address(xml, address, "ShipTo", include_street: true)
|
|
16
33
|
end
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Spree
|
|
4
4
|
module Integrations
|
|
5
|
+
##
|
|
6
|
+
# Per-store ShipStation credentials registered with Spree's integration framework.
|
|
7
|
+
#
|
|
5
8
|
class Shipstation < Spree::Integration
|
|
6
9
|
preference :username, :string
|
|
7
10
|
|
|
@@ -45,14 +48,29 @@ module Spree
|
|
|
45
48
|
},
|
|
46
49
|
allow_blank: true
|
|
47
50
|
|
|
51
|
+
##
|
|
52
|
+
# Admin grouping for this integration.
|
|
53
|
+
#
|
|
54
|
+
# @return [String]
|
|
55
|
+
#
|
|
48
56
|
def self.integration_group
|
|
49
57
|
"Shipping"
|
|
50
58
|
end
|
|
51
59
|
|
|
60
|
+
##
|
|
61
|
+
# Path to the logo shown in the admin integrations list.
|
|
62
|
+
#
|
|
63
|
+
# @return [String]
|
|
64
|
+
#
|
|
52
65
|
def self.icon_path
|
|
53
66
|
"integration_icons/shipstation-logo.webp"
|
|
54
67
|
end
|
|
55
68
|
|
|
69
|
+
##
|
|
70
|
+
# Translated brand name shown in the admin integrations list.
|
|
71
|
+
#
|
|
72
|
+
# @return [String]
|
|
73
|
+
#
|
|
56
74
|
def self.integration_name
|
|
57
75
|
Spree.t("admin.integrations.shipstation.brand_name")
|
|
58
76
|
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Spree
|
|
4
|
+
##
|
|
5
|
+
# Adds +exportable+ and +between+ scopes to +Spree::Shipment+ so the export
|
|
6
|
+
# endpoint can select ready shipments in a date window.
|
|
7
|
+
#
|
|
4
8
|
module ShipmentDecorator
|
|
5
9
|
def self.prepended(base)
|
|
6
10
|
base.scope :exportable, lambda {
|
|
@@ -3,46 +3,85 @@
|
|
|
3
3
|
module Spree
|
|
4
4
|
module Shipstation
|
|
5
5
|
module Export
|
|
6
|
+
##
|
|
6
7
|
# Shapes a shipment line item (a line item plus its inventory units) into the
|
|
7
|
-
# values ShipStation's export XML expects for an
|
|
8
|
+
# values ShipStation's export XML expects for an +<Item>+.
|
|
9
|
+
#
|
|
8
10
|
class ItemPresenter
|
|
9
11
|
attr_reader :line_item, :units
|
|
10
12
|
|
|
13
|
+
##
|
|
14
|
+
# @param line_item [Spree::LineItem] the line item
|
|
15
|
+
# @param units [Array<Spree::InventoryUnit>] inventory units for that line
|
|
16
|
+
#
|
|
11
17
|
def initialize(line_item, units)
|
|
12
18
|
@line_item = line_item
|
|
13
19
|
@units = units
|
|
14
20
|
end
|
|
15
21
|
|
|
22
|
+
##
|
|
23
|
+
# @return [Spree::Variant]
|
|
24
|
+
#
|
|
16
25
|
def variant
|
|
17
26
|
@variant ||= line_item.variant
|
|
18
27
|
end
|
|
19
28
|
|
|
29
|
+
##
|
|
30
|
+
# @return [String]
|
|
31
|
+
#
|
|
20
32
|
def sku
|
|
21
33
|
variant.sku
|
|
22
34
|
end
|
|
23
35
|
|
|
36
|
+
##
|
|
37
|
+
# Product name plus variant options, dropping blanks.
|
|
38
|
+
#
|
|
39
|
+
# @return [String]
|
|
40
|
+
#
|
|
24
41
|
def name
|
|
25
42
|
[variant.product.name, variant.options_text].reject(&:blank?).join(" ")
|
|
26
43
|
end
|
|
27
44
|
|
|
28
|
-
|
|
29
|
-
#
|
|
45
|
+
##
|
|
46
|
+
# The +Spree::Image+ to advertise, or +nil+. URL generation stays in the
|
|
47
|
+
# view because it depends on Rails route/URL helpers.
|
|
48
|
+
#
|
|
49
|
+
# @return [Spree::Image, NilClass]
|
|
50
|
+
#
|
|
30
51
|
def image
|
|
31
52
|
variant.images.first || variant.product.master.images.first
|
|
32
53
|
end
|
|
33
54
|
|
|
55
|
+
##
|
|
56
|
+
# @return [Spree::Shipstation::Export::Weight]
|
|
57
|
+
#
|
|
34
58
|
def weight
|
|
35
59
|
Weight.from_variant(variant)
|
|
36
60
|
end
|
|
37
61
|
|
|
62
|
+
##
|
|
63
|
+
# The number of items to ship, summed across the line's inventory units.
|
|
64
|
+
#
|
|
65
|
+
# Spree stores a quantity on each +Spree::InventoryUnit+ rather than one
|
|
66
|
+
# row per item, so counting rows under-reports any line ordered more
|
|
67
|
+
# than once.
|
|
68
|
+
#
|
|
69
|
+
# @return [Integer]
|
|
70
|
+
#
|
|
38
71
|
def quantity
|
|
39
|
-
units.
|
|
72
|
+
units.sum(&:quantity)
|
|
40
73
|
end
|
|
41
74
|
|
|
75
|
+
##
|
|
76
|
+
# @return [BigDecimal]
|
|
77
|
+
#
|
|
42
78
|
def unit_price
|
|
43
79
|
line_item.price
|
|
44
80
|
end
|
|
45
81
|
|
|
82
|
+
##
|
|
83
|
+
# @return [ActiveRecord::Relation, Array]
|
|
84
|
+
#
|
|
46
85
|
def option_values
|
|
47
86
|
variant.option_values
|
|
48
87
|
end
|
|
@@ -3,75 +3,131 @@
|
|
|
3
3
|
module Spree
|
|
4
4
|
module Shipstation
|
|
5
5
|
module Export
|
|
6
|
-
|
|
6
|
+
##
|
|
7
|
+
# Shapes a single +Spree::Shipment+ into the values ShipStation's export XML
|
|
7
8
|
# expects, keeping data-massaging logic out of the Builder template.
|
|
8
9
|
#
|
|
9
10
|
# Note: ShipStation models one "Order" per shipment, so most order-level
|
|
10
|
-
# fields are derived from
|
|
11
|
-
# shipment itself (mirroring the
|
|
11
|
+
# fields are derived from +shipment.order+ while identifiers come from the
|
|
12
|
+
# shipment itself (mirroring the +<OrderNumber>+ = shipment.number contract).
|
|
13
|
+
#
|
|
12
14
|
class OrderPresenter
|
|
13
15
|
attr_reader :shipment
|
|
14
16
|
|
|
17
|
+
##
|
|
18
|
+
# @param shipment [Spree::Shipment] the shipment to export
|
|
19
|
+
#
|
|
15
20
|
def initialize(shipment)
|
|
16
21
|
@shipment = shipment
|
|
17
22
|
end
|
|
18
23
|
|
|
24
|
+
##
|
|
25
|
+
# @return [Spree::Order]
|
|
26
|
+
#
|
|
19
27
|
def order
|
|
20
28
|
shipment.order
|
|
21
29
|
end
|
|
22
30
|
|
|
31
|
+
##
|
|
32
|
+
# @return [Integer]
|
|
33
|
+
#
|
|
23
34
|
def order_id
|
|
24
35
|
shipment.id
|
|
25
36
|
end
|
|
26
37
|
|
|
38
|
+
##
|
|
39
|
+
# @return [String]
|
|
40
|
+
#
|
|
27
41
|
def order_number
|
|
28
42
|
shipment.number
|
|
29
43
|
end
|
|
30
44
|
|
|
45
|
+
##
|
|
46
|
+
# @return [String]
|
|
47
|
+
#
|
|
31
48
|
def order_status
|
|
32
49
|
shipment.state
|
|
33
50
|
end
|
|
34
51
|
|
|
52
|
+
##
|
|
53
|
+
# @return [String, NilClass]
|
|
54
|
+
#
|
|
35
55
|
def order_date
|
|
36
56
|
format_date(order.completed_at)
|
|
37
57
|
end
|
|
38
58
|
|
|
59
|
+
##
|
|
60
|
+
# @return [String, NilClass]
|
|
61
|
+
#
|
|
39
62
|
def last_modified
|
|
40
63
|
format_date([order.completed_at, shipment.updated_at].compact.max)
|
|
41
64
|
end
|
|
42
65
|
|
|
66
|
+
##
|
|
67
|
+
# @return [String, NilClass]
|
|
68
|
+
#
|
|
43
69
|
def shipping_method_name
|
|
44
70
|
shipment.shipping_method&.name
|
|
45
71
|
end
|
|
46
72
|
|
|
73
|
+
##
|
|
74
|
+
# @return [BigDecimal]
|
|
75
|
+
#
|
|
47
76
|
def order_total
|
|
48
77
|
order.total
|
|
49
78
|
end
|
|
50
79
|
|
|
80
|
+
##
|
|
81
|
+
# @return [BigDecimal]
|
|
82
|
+
#
|
|
51
83
|
def tax_total
|
|
52
84
|
order.tax_total
|
|
53
85
|
end
|
|
54
86
|
|
|
87
|
+
##
|
|
88
|
+
# @return [BigDecimal]
|
|
89
|
+
#
|
|
55
90
|
def ship_total
|
|
56
91
|
order.ship_total
|
|
57
92
|
end
|
|
58
93
|
|
|
94
|
+
##
|
|
95
|
+
# Spree order number, exported as ShipStation CustomField1.
|
|
96
|
+
#
|
|
97
|
+
# @return [String]
|
|
98
|
+
#
|
|
59
99
|
def custom_field_1
|
|
60
100
|
order.number
|
|
61
101
|
end
|
|
62
102
|
|
|
103
|
+
##
|
|
104
|
+
# Email truncated to ShipStation's 50-character customer-code limit.
|
|
105
|
+
#
|
|
106
|
+
# @return [String, NilClass]
|
|
107
|
+
#
|
|
63
108
|
def customer_code
|
|
64
109
|
order.email&.slice(0, 50)
|
|
65
110
|
end
|
|
66
111
|
|
|
112
|
+
##
|
|
113
|
+
# @return [Spree::Address, NilClass]
|
|
114
|
+
#
|
|
67
115
|
def bill_address
|
|
68
116
|
order.bill_address
|
|
69
117
|
end
|
|
70
118
|
|
|
119
|
+
##
|
|
120
|
+
# @return [Spree::Address, NilClass]
|
|
121
|
+
#
|
|
71
122
|
def ship_address
|
|
72
123
|
order.ship_address
|
|
73
124
|
end
|
|
74
125
|
|
|
126
|
+
##
|
|
127
|
+
# One presenter per line item that still has a variant.
|
|
128
|
+
#
|
|
129
|
+
# @return [Array<Spree::Shipstation::Export::ItemPresenter>]
|
|
130
|
+
#
|
|
75
131
|
def items
|
|
76
132
|
shipment.inventory_units.group_by(&:line_item).filter_map do |line_item, units|
|
|
77
133
|
next unless line_item.variant
|
|
@@ -3,19 +3,31 @@
|
|
|
3
3
|
module Spree
|
|
4
4
|
module Shipstation
|
|
5
5
|
module Export
|
|
6
|
-
|
|
6
|
+
##
|
|
7
|
+
# Value object converting a +Spree::Variant+'s weight into the
|
|
7
8
|
# (value, unit-label) pair ShipStation's export XML expects.
|
|
8
9
|
#
|
|
9
10
|
# ShipStation accepts Pounds, Ounces, and Grams; any unrecognised Spree
|
|
10
11
|
# weight unit (including a missing weight) falls back to Grams.
|
|
12
|
+
#
|
|
11
13
|
class Weight
|
|
12
14
|
attr_reader :value, :units
|
|
13
15
|
|
|
16
|
+
##
|
|
17
|
+
# @param value [Float] numeric weight
|
|
18
|
+
# @param units [String] ShipStation unit label
|
|
19
|
+
#
|
|
14
20
|
def initialize(value:, units:)
|
|
15
21
|
@value = value
|
|
16
22
|
@units = units
|
|
17
23
|
end
|
|
18
24
|
|
|
25
|
+
##
|
|
26
|
+
# Converts a variant's weight into ShipStation units.
|
|
27
|
+
#
|
|
28
|
+
# @param variant [Spree::Variant]
|
|
29
|
+
# @return [Spree::Shipstation::Export::Weight]
|
|
30
|
+
#
|
|
19
31
|
def self.from_variant(variant)
|
|
20
32
|
amount = (variant.weight || 0.0).to_f
|
|
21
33
|
|
|
@@ -31,11 +43,18 @@ module Spree
|
|
|
31
43
|
end
|
|
32
44
|
end
|
|
33
45
|
|
|
46
|
+
##
|
|
47
|
+
# @param other [Object]
|
|
48
|
+
# @return [TrueClass, FalseClass]
|
|
49
|
+
#
|
|
34
50
|
def ==(other)
|
|
35
51
|
other.is_a?(self.class) && value == other.value && units == other.units
|
|
36
52
|
end
|
|
37
53
|
alias_method :eql?, :==
|
|
38
54
|
|
|
55
|
+
##
|
|
56
|
+
# @return [Integer]
|
|
57
|
+
#
|
|
39
58
|
def hash
|
|
40
59
|
[value, units].hash
|
|
41
60
|
end
|
|
@@ -2,20 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
module Spree
|
|
4
4
|
module Shipstation
|
|
5
|
+
##
|
|
6
|
+
# Base error for ShipStation integration failures returned as HTTP 400.
|
|
7
|
+
#
|
|
5
8
|
class Error < StandardError; end
|
|
6
9
|
|
|
10
|
+
##
|
|
11
|
+
# Raised when the shipnotify payload names a shipment that does not exist.
|
|
12
|
+
#
|
|
7
13
|
class ShipmentNotFoundError < Error
|
|
14
|
+
##
|
|
15
|
+
# @param shipment_number [String] the shipment number from the payload
|
|
16
|
+
#
|
|
8
17
|
def initialize(shipment_number, *args)
|
|
9
18
|
super("Could not find shipment with number #{shipment_number}", *args)
|
|
10
19
|
end
|
|
11
20
|
end
|
|
12
21
|
|
|
22
|
+
##
|
|
23
|
+
# Raised when a pending payment cannot be captured during shipnotify.
|
|
24
|
+
#
|
|
13
25
|
class PaymentError < Error
|
|
26
|
+
##
|
|
27
|
+
# @param payment [Spree::Payment] the payment that failed to capture
|
|
28
|
+
#
|
|
14
29
|
def initialize(payment, *args)
|
|
15
30
|
super("Could not process payment #{payment.id}", *args)
|
|
16
31
|
end
|
|
17
32
|
end
|
|
18
33
|
|
|
34
|
+
##
|
|
35
|
+
# Raised when shipnotify is called without a tracking number.
|
|
36
|
+
#
|
|
19
37
|
class MissingTrackingNumberError < Error
|
|
20
38
|
def initialize(*args)
|
|
21
39
|
super("Tracking number is required", *args)
|
|
@@ -2,14 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
module Spree
|
|
4
4
|
module Shipstation
|
|
5
|
+
##
|
|
6
|
+
# Applies a ShipStation shipnotify payload to a Spree shipment.
|
|
7
|
+
#
|
|
8
|
+
# Looks up the shipment by number on the given store and, inside a
|
|
9
|
+
# transaction: captures pending payments if +auto_capture_on_dispatch+ is
|
|
10
|
+
# on, writes the tracking number, and ships the shipment unless it is
|
|
11
|
+
# already shipped.
|
|
12
|
+
#
|
|
5
13
|
class ShipmentNotice
|
|
6
14
|
attr_reader :shipment_number, :shipment_tracking, :store
|
|
7
15
|
|
|
8
16
|
class << self
|
|
17
|
+
##
|
|
18
|
+
# Builds a notice from the shipnotify webhook params.
|
|
19
|
+
#
|
|
20
|
+
# ShipStation's webhook param is named +order_number+ but its value is
|
|
21
|
+
# the shipment number — it mirrors the +<OrderNumber>+ field from the
|
|
22
|
+
# export XML.
|
|
23
|
+
#
|
|
24
|
+
# @param params [Hash] webhook params including +:order_number+ and +:tracking_number+
|
|
25
|
+
# @param store [Spree::Store] store that owns the shipment
|
|
26
|
+
# @return [Spree::Shipstation::ShipmentNotice]
|
|
27
|
+
#
|
|
9
28
|
def from_payload(params, store:)
|
|
10
29
|
new(
|
|
11
|
-
# ShipStation's webhook param is named `order_number` but its value is the
|
|
12
|
-
# shipment number — it mirrors the <OrderNumber> field from the export XML.
|
|
13
30
|
shipment_number: params[:order_number],
|
|
14
31
|
shipment_tracking: params[:tracking_number],
|
|
15
32
|
store: store
|
|
@@ -17,12 +34,25 @@ module Spree
|
|
|
17
34
|
end
|
|
18
35
|
end
|
|
19
36
|
|
|
37
|
+
##
|
|
38
|
+
# @param shipment_number [String] Spree shipment number
|
|
39
|
+
# @param shipment_tracking [String] carrier tracking number
|
|
40
|
+
# @param store [Spree::Store] store that owns the shipment
|
|
41
|
+
#
|
|
20
42
|
def initialize(shipment_number:, shipment_tracking:, store:)
|
|
21
43
|
@shipment_number = shipment_number
|
|
22
44
|
@shipment_tracking = shipment_tracking
|
|
23
45
|
@store = store
|
|
24
46
|
end
|
|
25
47
|
|
|
48
|
+
##
|
|
49
|
+
# Captures pending payments if configured, writes tracking, and ships.
|
|
50
|
+
#
|
|
51
|
+
# @return [Spree::Shipment] the updated shipment
|
|
52
|
+
# @raise [Spree::Shipstation::ShipmentNotFoundError] when the shipment is missing
|
|
53
|
+
# @raise [Spree::Shipstation::MissingTrackingNumberError] when tracking is blank
|
|
54
|
+
# @raise [Spree::Shipstation::PaymentError] when a pending payment cannot be captured
|
|
55
|
+
#
|
|
26
56
|
def apply
|
|
27
57
|
raise ShipmentNotFoundError, shipment_number unless shipment
|
|
28
58
|
|
|
@@ -3,8 +3,13 @@
|
|
|
3
3
|
module Spree
|
|
4
4
|
module Shipstation
|
|
5
5
|
# Major version tracks Spree's major version: 5.x supports Spree 5.x.
|
|
6
|
-
VERSION = "5.0.
|
|
6
|
+
VERSION = "5.0.1"
|
|
7
7
|
|
|
8
|
+
##
|
|
9
|
+
# Gem version as a comparable +Gem::Version+.
|
|
10
|
+
#
|
|
11
|
+
# @return [Gem::Version]
|
|
12
|
+
#
|
|
8
13
|
def self.version
|
|
9
14
|
Gem::Version.new(VERSION)
|
|
10
15
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree-shipstation
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.0.
|
|
4
|
+
version: 5.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthew Kennedy
|
|
@@ -72,6 +72,7 @@ extra_rdoc_files: []
|
|
|
72
72
|
files:
|
|
73
73
|
- ".github/stale.yml"
|
|
74
74
|
- ".github/workflows/lint.yml"
|
|
75
|
+
- ".github/workflows/release.yml"
|
|
75
76
|
- ".github/workflows/security.yml"
|
|
76
77
|
- ".github/workflows/test.yml"
|
|
77
78
|
- ".gitignore"
|
|
@@ -80,6 +81,7 @@ files:
|
|
|
80
81
|
- Appraisals
|
|
81
82
|
- CHANGELOG.md
|
|
82
83
|
- CLAUDE.md
|
|
84
|
+
- CODE_OF_CONDUCT.md
|
|
83
85
|
- Gemfile
|
|
84
86
|
- LICENSE
|
|
85
87
|
- README.md
|
|
@@ -134,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
134
136
|
- !ruby/object:Gem::Version
|
|
135
137
|
version: '0'
|
|
136
138
|
requirements: []
|
|
137
|
-
rubygems_version: 4.0.
|
|
139
|
+
rubygems_version: 4.0.16
|
|
138
140
|
specification_version: 4
|
|
139
141
|
summary: ShipStation integration for Spree e-commerce
|
|
140
142
|
test_files: []
|