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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 30a88f9939731dc679980577dc0b23a91fdbda25c4019063b3f4728dfb2fe5a9
4
- data.tar.gz: d410ae3a207dd0051ab938c3890c60c515b403723ac43392ede48ab872352ca1
3
+ metadata.gz: 59253a22e0dcd58163a5f9e3380f1e1c502f8256e05f7b2d65755b01a4879ec8
4
+ data.tar.gz: eb36256ce70cc0ab15858669bf823b4435f2a8d167a0517425af042db83dd95d
5
5
  SHA512:
6
- metadata.gz: f1d456fbe2b2e4a682b7cc2b62e1c07faad779b449bd8f2da72e5a2ce67b9f133b8cb23a214bb62dfd2ba4d1429a6b1b6fc8839597feb3e445a31fd419de5869
7
- data.tar.gz: ce40b12b11650ef36d848a709a4a0bd513c654a9ca6da8659728e178dd82cdba573504dbaf7ce981ef67509b986d0a11f47157f21280894dec8c90f2fae56769
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
@@ -50,6 +50,9 @@ bundle exec standardrb
50
50
  bundle exec standardrb --fix
51
51
  ```
52
52
 
53
+ Public Ruby classes and methods use YARD `##` blocks. Specs follow one
54
+ expectation per example.
55
+
53
56
  ## Architecture
54
57
 
55
58
  ### Integration Registration
@@ -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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
 
3
5
  gem "appraisal"
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,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler"
2
4
  Bundler::GemHelper.install_tasks
3
5
 
@@ -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 <Item>.
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
- # The Spree::Image to advertise, or nil. URL generation stays in the view
29
- # because it depends on Rails route/URL helpers.
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.size
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
- # Shapes a single Spree::Shipment into the values ShipStation's export XML
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 `shipment.order` while identifiers come from the
11
- # shipment itself (mirroring the <OrderNumber> = shipment.number contract).
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
- # Value object converting a Spree::Variant's weight into the
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Rails.application.config.after_initialize do
2
4
  Rails.application.config.spree.integrations << Spree::Integrations::Shipstation
3
5
  end
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Spree
4
4
  module Shipstation
5
+ ##
6
+ # Rails engine that loads decorator files and registers ShipStation assets.
7
+ #
5
8
  class Engine < ::Rails::Engine
6
9
  require "spree/core"
7
10
  isolate_namespace Spree
@@ -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
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  FactoryBot.define do
2
4
  factory :shipstation_integration, class: Spree::Integrations::Shipstation do
3
5
  active { true }
@@ -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.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.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.15
139
+ rubygems_version: 4.0.16
138
140
  specification_version: 4
139
141
  summary: ShipStation integration for Spree e-commerce
140
142
  test_files: []