spree_doordash 0.1.1 → 0.1.3

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: 11ea8c9082c5f3a17fb5e7ad172eab82c717d17af077bee7d0df14c92a75fba6
4
- data.tar.gz: 5e435d16a2bef4e610760fb993533cf54f31a81343b1f5ade02f7889f8eb622c
3
+ metadata.gz: 6a028415e34fd065f59a11e00034d88ef6ea2dfa8155538bf61f63ba26cf16ed
4
+ data.tar.gz: f0425700d9f63218ee14eae64ce30d6e444c46b2eefae2fe4d3854f38426e13a
5
5
  SHA512:
6
- metadata.gz: bcee352d7bc9b0a6eeb877d5cd112cf40de922de796739f607058ad92735c787f665641d981d501e3d21af5b82b7e8f4e289b4a96cc6351d659d9f78eed95174
7
- data.tar.gz: 99e77a1d48d37fe6f1573341a0e93d90f4d7a68690ede1f1ecdfc43c508340ef9b60aec101f9c4b7aea32fca25109dc94e8f13f8712124802f8b4e64e7866295
6
+ metadata.gz: 25f7dc0b927d237844b908072f4b3f9fa738fe3db1c6b4036280e51d739c6b5eea92c2578dc730dbd5c89b33de5a10318f9898ee91e295a55deafcdf3426ae5b
7
+ data.tar.gz: b32fb99c09cb7344185f06ca15c72c60253669b0d5ea5374de62490c4a62e007ce39589b6df4a491bcfd8a54e71c4caf266ea348c0b8d9abf1355b01c03dcd23
data/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  All notable changes to this project are documented here.
4
4
 
5
+ ## 0.1.3
6
+
7
+ A real bug found live: the `doordash_quote_unavailable` warning added in 0.1.2 never actually
8
+ worked in production. `Spree::Calculator::Shipping::DoordashQuote#compute_package` pushed it onto
9
+ `package.order.warnings` — but `package.order` is **not** the same in-memory object as the order
10
+ the caller (`Spree::Order#create_proposed_shipments`) holds. `spree_core`'s own
11
+ `Spree::Stock::InventoryUnitBuilder#units` builds each inventory unit with `order_id: @order.id`
12
+ but deliberately *not* `order: @order` ("avoid loading the association to order until needed," per
13
+ its own comment) — the first thing that touches `.order` on one of those units triggers a fresh
14
+ `Spree::Order.find`, a brand-new Ruby object. So the warning was mutating a throwaway copy,
15
+ discarded the instant `compute_package` returned, and never reached the order this request
16
+ actually serializes back to the storefront as `cart.warnings`. Confirmed via `object_id` tracing
17
+ against production, not assumed — and confirmed the existing spec suite couldn't have caught it:
18
+ `doordash_quote_spec.rb`'s own `instance_double(Spree::Stock::Package, order: order)` makes
19
+ `package.order` the *same* object as the test's `order` by construction, sidestepping the exact
20
+ boundary that breaks in real Spree core.
21
+
22
+ Fixed by bridging across that boundary with the order's stable id instead of Ruby object identity
23
+ — `Spree::Calculator::Shipping::DoordashQuote.mark_unavailable(order_id)` sets a thread-local flag
24
+ (chosen over `Rails.cache`: this gem installs into arbitrary host apps, some of which legitimately
25
+ run `config.cache_store = :null_store`, which would silently degrade this back to the original bug
26
+ with zero indication anything was wrong — `compute_package` and `create_proposed_shipments` always
27
+ run synchronously on the same thread within one request, so a thread-local needs no external store
28
+ and no expiry logic). A new `Spree::OrderDecorator#create_proposed_shipments` reads the flag back
29
+ and merges the warning onto `self.warnings` immediately after `super` returns — `self` there *is*
30
+ the real order, since `create_proposed_shipments` is called directly on it by the checkout flow.
31
+ The flag is cleared unconditionally at the *start* of every call (not just after a successful
32
+ merge), so a stale flag left behind by an earlier request that raised before reaching the merge
33
+ step can never leak into a later, unrelated call that happens to reuse the same thread.
34
+
35
+ New `spec/models/spree/order_decorator_spec.rb` exercises the real
36
+ `order_routing_strategy -> Estimator -> Packer -> InventoryUnitBuilder` path end to end (only
37
+ `SpreeDoordash::Quote.call` is stubbed) — the only way to actually exercise the object-identity
38
+ boundary the fix bridges across, rather than doubling it away. Full suite: 95 examples, 0 failures
39
+ (5 new). Coverage: 94.14%. Brakeman clean.
40
+
41
+ ## 0.1.2
42
+
43
+ A real bug found live during checkout-validation work: a blank phone on the dropoff address used
44
+ to fall back to a fake placeholder number (`+10000000000`) that DoorDash's API rejected anyway —
45
+ the request was doomed before it was even built, and (same as the E.164 issue fixed in 0.1.1) the
46
+ DoorDash Delivery rate just silently vanished with no error anywhere in the UI.
47
+
48
+ - `SpreeDoordash::Quote#call` now returns early (no API call at all) when the ship address has no
49
+ phone, instead of building a request known to fail. `format_phone`'s placeholder fallback is
50
+ removed entirely.
51
+ - `Spree::Calculator::Shipping::DoordashQuote#compute_package` now pushes a
52
+ `doordash_quote_unavailable` warning onto `Spree::Order#warnings` whenever `SpreeDoordash::Quote`
53
+ returns `nil` — generic to *why* the quote failed (bad/missing phone, an address DoorDash
54
+ genuinely can't serve, DoorDash API down), confirmed live for two different real causes.
55
+ `Spree::Order#warnings` is the same transient mechanism core's own
56
+ `ensure_available_shipping_rates` already uses for the identical class of problem, and flows
57
+ through to the Store API's `cart.warnings` with no new API surface. A storefront reference
58
+ implementation now renders this as a real, non-blocking banner instead of silence — see
59
+ `spree_storefront_web`'s `DeliveryMethodSection.tsx`.
60
+
61
+ Full suite: 90 examples, 0 failures (2 new). Coverage: 94.14% (241/256 lines). Brakeman clean.
62
+
5
63
  ## 0.1.1
6
64
 
7
65
  Two real production bugs, both invisible to the full spec suite and to every prior "live
@@ -20,11 +20,62 @@ module Spree
20
20
 
21
21
  def compute_package(package)
22
22
  result = SpreeDoordash::Quote.call(package.order)
23
- return nil unless result
23
+ if result.nil?
24
+ # A real bug found live: a nil Result here (bad/missing phone, an
25
+ # address DoorDash genuinely can't serve, DoorDash API down —
26
+ # SpreeDoordash::Quote#call collapses all of these to the same
27
+ # "unavailable" nil, deliberately, so this stays generic to *why*)
28
+ # used to just make the rate silently vanish with zero signal
29
+ # anywhere in the UI. Spree::Order#warnings is the same
30
+ # transient, request-scoped mechanism core's own
31
+ # `ensure_available_shipping_rates` already uses for the
32
+ # identical class of problem (a line item that can't ship at
33
+ # all) — it flows through to the Store API's `cart.warnings`
34
+ # with no new API surface needed. The storefront renders its own
35
+ # localized copy for this code; `message` here is English-only,
36
+ # a fallback for any other API consumer, not the UI text itself.
37
+ #
38
+ # A SECOND real bug found live (v0.1.2, this fix is v0.1.3):
39
+ # package.order is not the same object as the order the caller
40
+ # holds (spree_core's InventoryUnitBuilder deliberately defers
41
+ # loading that association — see the Spree::OrderDecorator this
42
+ # gem ships for the full story), so pushing the warning directly
43
+ # onto package.order.warnings here mutated a throwaway copy that
44
+ # never reached the real order. Thread.current bridges across
45
+ # that boundary via the order's id instead of Ruby object
46
+ # identity — chosen over Rails.cache deliberately: this gem
47
+ # installs into arbitrary host apps, some of which run
48
+ # `config.cache_store = :null_store` (the Rails test-env
49
+ # default, and a legitimate production choice too), which would
50
+ # silently degrade this back to the original bug with no
51
+ # indication anything was wrong. compute_package and
52
+ # create_proposed_shipments always run synchronously on the same
53
+ # thread within one request (confirmed: this is the exact call
54
+ # chain reproduced live to root-cause the original bug), so a
55
+ # thread-local flag needs no external store and no expiry logic
56
+ # — Spree::OrderDecorator clears it unconditionally at the start
57
+ # of every create_proposed_shipments call, so a stale flag from
58
+ # an earlier request that errored out before consuming it can
59
+ # never leak into a later, unrelated one.
60
+ self.class.mark_unavailable(package.order.id)
61
+ return nil
62
+ end
24
63
 
25
64
  result.fee_cents / 100.0
26
65
  end
27
66
 
67
+ def self.mark_unavailable(order_id)
68
+ (Thread.current[:spree_doordash_quote_unavailable_order_ids] ||= []) << order_id
69
+ end
70
+
71
+ def self.unavailable?(order_id)
72
+ Thread.current[:spree_doordash_quote_unavailable_order_ids]&.include?(order_id) || false
73
+ end
74
+
75
+ def self.clear_unavailable(order_id)
76
+ Thread.current[:spree_doordash_quote_unavailable_order_ids]&.delete(order_id)
77
+ end
78
+
28
79
  # Called by Estimator to filter which shipping methods even attempt
29
80
  # a compute_package call. Skips the DoorDash API round-trip entirely
30
81
  # for an order with no ship address yet (early checkout, before
@@ -0,0 +1,54 @@
1
+ module Spree
2
+ # Bridges Spree::Calculator::Shipping::DoordashQuote#compute_package's
3
+ # doordash_quote_unavailable warning back onto the real order object.
4
+ #
5
+ # A real bug found live (confirmed via object_id tracing against
6
+ # production, not assumed): `compute_package` receives a
7
+ # `Spree::Stock::Package`, and `package.order` is NOT the same in-memory
8
+ # object as `self` in `create_proposed_shipments` below. spree_core's own
9
+ # `Spree::Stock::InventoryUnitBuilder#units` builds each inventory unit
10
+ # with `order_id: @order.id` but deliberately *not* `order: @order` — its
11
+ # own comment says why: "avoid loading the association to order until
12
+ # needed." The first thing that touches `.order` on one of those units (or
13
+ # a Package built from them) triggers a fresh `Spree::Order.find`, a
14
+ # brand-new Ruby object. So `package.order.warnings |= [...]` inside the
15
+ # calculator was mutating a throwaway copy that's discarded the instant
16
+ # `compute_package` returns — the warning never reached the order object
17
+ # this request actually serializes back to the storefront as
18
+ # `cart.warnings`. `spree_doordash` v0.1.2's warning never worked in
19
+ # production despite passing its own specs (the dummy app's specs call
20
+ # the calculator directly against a single order instance, which can't
21
+ # exhibit this — it only shows up through the real
22
+ # create_proposed_shipments -> order_routing_strategy -> Estimator path).
23
+ #
24
+ # Fix: since Ruby object identity doesn't survive that boundary, bridge
25
+ # across it with the order's stable id instead, via a thread-local flag
26
+ # (see DoordashQuote.mark_unavailable/unavailable?/clear_unavailable for
27
+ # why Thread.current was chosen over Rails.cache). Cleared unconditionally
28
+ # at the *start* of every call — not just after a successful merge — so a
29
+ # stale flag left behind by an earlier request that raised before
30
+ # reaching the merge step can never leak into a later, unrelated
31
+ # create_proposed_shipments call that happens to reuse the same thread.
32
+ module OrderDecorator
33
+ def create_proposed_shipments
34
+ Spree::Calculator::Shipping::DoordashQuote.clear_unavailable(id)
35
+ result = super
36
+ merge_doordash_quote_warning!
37
+ result
38
+ end
39
+
40
+ private
41
+
42
+ def merge_doordash_quote_warning!
43
+ return unless Spree::Calculator::Shipping::DoordashQuote.unavailable?(id)
44
+
45
+ Spree::Calculator::Shipping::DoordashQuote.clear_unavailable(id)
46
+ self.warnings |= [{
47
+ code: 'doordash_quote_unavailable',
48
+ message: 'We could not get a DoorDash delivery quote for this address.'
49
+ }]
50
+ end
51
+ end
52
+
53
+ Order.prepend OrderDecorator
54
+ end
@@ -17,6 +17,16 @@ module SpreeDoordash
17
17
  def call(order)
18
18
  location_mapping = location_mapping_for(order)
19
19
  return nil unless location_mapping && order.ship_address
20
+ # A real bug found live: with no phone on the ship address,
21
+ # `format_phone` used to fall back to a fake placeholder number that
22
+ # DoorDash's API correctly rejects with a 400 — the request was
23
+ # doomed before it was even built. `Spree::Config[:address_requires_phone]`
24
+ # (spree_host) should make this unreachable in the normal storefront
25
+ # checkout flow, but this guard stays as defense in depth for any
26
+ # other path that can create an order (admin, API, migrated data)
27
+ # — same "unavailable, not exceptional" nil-return shape as every
28
+ # other unserviceable case below.
29
+ return nil if order.ship_address.phone.blank?
20
30
 
21
31
  client = SpreeDoordash::Client.for_store(order.store || Spree::Store.default)
22
32
  # A random suffix per attempt, not just order.number — confirmed live
@@ -93,11 +103,17 @@ module SpreeDoordash
93
103
  # US-only, matching the rest of this demo (every address here is a US
94
104
  # address) — strips everything but digits, assumes a bare 10-digit
95
105
  # number is domestic and prepends the country code.
106
+ #
107
+ # No blank-phone fallback here (a prior version faked one as
108
+ # "+10000000000" — DoorDash rejected it too, just as uninformatively as
109
+ # a missing one; better to let a genuinely blank pickup phone fail
110
+ # DoorDash's own validation loudly than paper over it with fake data).
111
+ # `call` above already guards the dropoff phone specifically; this
112
+ # method is also reused for the pickup (StockLocation) phone, which is
113
+ # real business data expected to always be set.
96
114
  def format_phone(raw)
97
115
  digits = raw.to_s.gsub(/\D/, '')
98
116
  digits = "1#{digits}" if digits.length == 10
99
- return '+10000000000' if digits.blank?
100
-
101
117
  "+#{digits}"
102
118
  end
103
119
 
@@ -1,5 +1,5 @@
1
1
  module SpreeDoordash
2
- VERSION = '0.1.1'.freeze
2
+ VERSION = '0.1.3'.freeze
3
3
 
4
4
  def gem_version
5
5
  Gem::Version.new(VERSION)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_doordash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amit Solanki
@@ -120,6 +120,7 @@ files:
120
120
  - app/jobs/spree_doordash/delivery_dispatch_job.rb
121
121
  - app/jobs/spree_doordash/delivery_webhook_job.rb
122
122
  - app/models/spree/calculator/shipping/doordash_quote.rb
123
+ - app/models/spree/order_decorator.rb
123
124
  - app/models/spree_doordash/credential.rb
124
125
  - app/models/spree_doordash/delivery_mapping.rb
125
126
  - app/models/spree_doordash/location_mapping.rb