digiwin_dsp 0.4.2 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -1
- data/README.md +7 -3
- data/lib/digiwin_dsp/client.rb +37 -9
- data/lib/digiwin_dsp/configuration.rb +7 -1
- data/lib/digiwin_dsp/version.rb +1 -1
- data/lib/digiwin_dsp/webhooks/event.rb +7 -2
- data/lib/digiwin_dsp/webhooks/inventory_update.rb +7 -0
- data/lib/digiwin_dsp/webhooks/logistics_update.rb +7 -0
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 813b05b6b115eed5a1fd308aa3c8a255da6df70afdf090101e7bfe0f74f605e7
|
|
4
|
+
data.tar.gz: 7ecfa001245bff0c95125f9667e303506ab9c1170bf8cce12f039c9773715bb4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1086238addb8b6feb861d80f4c133c9e891e4b64ed45821ffc0dd68bde3a8e0ea7fadd009ee2b34dfa78e9a640fe2b22d8590a1b364d80b43081688771b53ebd
|
|
7
|
+
data.tar.gz: 12079e2663087764b54acae5b774e22398dc9ec48b3379684bfd6fdb0be51ef2604ac7d2db30e59bb7a9dae6bfdbac62b965c5c29b1ac1c1c7bf0c8247ab9fb2
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.5.0] - 2026-10-01
|
|
10
|
+
|
|
11
|
+
### BREAKING
|
|
12
|
+
|
|
13
|
+
- **Ruby 3.2 support dropped; supported range is now 3.3 – 4.0.** `required_ruby_version` is `>= 3.3.0` and CI runs 3.3 / 3.4 / 4.0. The dev-only `parallel < 2.0` pin (kept for the 3.2 CI row) is removed.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **POSTs are no longer resent after a read timeout.** faraday-retry's defaults retried `Faraday::TimeoutError` for every method, so a slow DSP that had already created the order would see a resend and the caller would get a spurious `DuplicateRequestError` (or a real duplicate on invoice/return, which have no documented dedupe). Only failures that happen before the request is sent (connection refused, host/network unreachable, DNS failure, connect timeout) are retried now — which also makes the README's "connection failures" claim true; previously `Faraday::ConnectionFailed` was never retried.
|
|
18
|
+
- **Non-object 2xx bodies raise `ServerError`** instead of being returned and blowing up as `NoMethodError` in `Resources::Base#create` / `WebhookSubscription#create` (outside the `DigiwinDsp::Error` tree, so `rescue`/`retry_on` missed it). A `{"std_data":"..."}` body no longer raises `TypeError` either.
|
|
19
|
+
- **Malformed inbound webhooks raise `Webhooks::ParseError`** instead of `TypeError` when an envelope level isn't an object, and `InventoryUpdate` / `LogisticsUpdate` now reject a non-object `request` payload.
|
|
20
|
+
- **`Configuration#environment=` accepts strings** (e.g. `c.environment = Rails.env`), matching the `DIGIWIN_DSP_ENV` path.
|
|
21
|
+
|
|
9
22
|
## [0.4.2] - 2026-06-18
|
|
10
23
|
|
|
11
24
|
### Added
|
|
@@ -264,7 +277,8 @@ Initial release. Covers the four Self-hosted Website Module (自有官網模組)
|
|
|
264
277
|
- The gem is **synchronous on purpose**. Callers wrap requests in their own background job runner (e.g. ActiveJob) when needed.
|
|
265
278
|
- Idempotency: clients can send `X-Idempotency-Key` via the `idempotency_key:` kwarg. DSP also dedupes server-side by `form_no + platform_id`.
|
|
266
279
|
|
|
267
|
-
[Unreleased]: https://github.com/7a6163/digiwin_dsp/compare/v0.
|
|
280
|
+
[Unreleased]: https://github.com/7a6163/digiwin_dsp/compare/v0.5.0...HEAD
|
|
281
|
+
[0.5.0]: https://github.com/7a6163/digiwin_dsp/compare/v0.4.2...v0.5.0
|
|
268
282
|
[0.4.2]: https://github.com/7a6163/digiwin_dsp/compare/v0.4.1...v0.4.2
|
|
269
283
|
[0.4.1]: https://github.com/7a6163/digiwin_dsp/compare/v0.4.0...v0.4.1
|
|
270
284
|
[0.4.0]: https://github.com/7a6163/digiwin_dsp/compare/v0.3.1...v0.4.0
|
data/README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# digiwin_dsp
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/rb/digiwin_dsp)
|
|
3
4
|
[](https://github.com/7a6163/digiwin_dsp/actions/workflows/ci.yml)
|
|
4
5
|
[](https://codecov.io/gh/7a6163/digiwin_dsp)
|
|
5
|
-
[](https://www.ruby-lang.org/)
|
|
6
7
|
[](./LICENSE.txt)
|
|
7
8
|
|
|
9
|
+
Homepage: <https://rubygems.org/gems/digiwin_dsp>
|
|
10
|
+
|
|
8
11
|
Ruby client for the Digiwin DSP Self-hosted Website Module (自有官網模組) API. Lets your storefront push orders, cancellations, invoice updates, and returns into the Digiwin ERP through the DSP gateway.
|
|
9
12
|
|
|
10
13
|
| Operation | Resource | Endpoint |
|
|
@@ -29,7 +32,7 @@ gem "digiwin_dsp"
|
|
|
29
32
|
bundle install
|
|
30
33
|
```
|
|
31
34
|
|
|
32
|
-
Ruby ≥ 3.
|
|
35
|
+
Ruby 3.3 – 4.0 supported (≥ 3.3 required).
|
|
33
36
|
|
|
34
37
|
## Configuration
|
|
35
38
|
|
|
@@ -227,7 +230,8 @@ Every `Client#post` already retries transparently inside Faraday:
|
|
|
227
230
|
| Setting | Value |
|
|
228
231
|
|---|---|
|
|
229
232
|
| Attempts | up to 4 (1 original + max 3 retries) |
|
|
230
|
-
| Triggers | HTTP 429, 500, 502, 503, 504
|
|
233
|
+
| Triggers | HTTP 429, 500, 502, 503, 504; connection failures that happen *before* the request reaches DSP (refused, host/network unreachable, DNS failure, connect timeout) |
|
|
234
|
+
| Never retried | Read timeouts and mid-request connection resets — DSP may already have processed the POST, so resending could duplicate it. These raise `NetworkError`; check DSP state before retrying |
|
|
231
235
|
| Backoff | exponential — ~0.5s, ~1s, ~2s between attempts, ±50% jitter |
|
|
232
236
|
|
|
233
237
|
So one `Resources::Order.create` call can take up to ~`4 × timeout + 3.5s` in the worst case (default `timeout` 10s → ~44s). **Size your job timeouts and queue latency budgets accordingly** — if you also add `retry_on` in ActiveJob/Sidekiq (recommended for `RateLimitError`, which DSP signals via the envelope and the gem does *not* retry internally), the two layers multiply.
|
data/lib/digiwin_dsp/client.rb
CHANGED
|
@@ -13,6 +13,29 @@ module DigiwinDsp
|
|
|
13
13
|
RETRY_INTERVAL = 0.5
|
|
14
14
|
RETRY_BACKOFF_FACTOR = 2
|
|
15
15
|
RETRY_INTERVAL_RANDOMNESS = 0.5
|
|
16
|
+
# Transport failures that happen before the request reaches DSP, so a
|
|
17
|
+
# resend can't duplicate a POST. Read timeouts, ECONNRESET, EPIPE etc.
|
|
18
|
+
# are deliberately absent: DSP may already have processed the request.
|
|
19
|
+
PRE_SEND_ERRORS = [
|
|
20
|
+
Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ENETUNREACH, SocketError, Net::OpenTimeout
|
|
21
|
+
].freeze
|
|
22
|
+
RETRY_IF = lambda do |_env, error|
|
|
23
|
+
!error.is_a?(Faraday::ConnectionFailed) ||
|
|
24
|
+
PRE_SEND_ERRORS.any? { |klass| error.wrapped_exception.is_a?(klass) }
|
|
25
|
+
end
|
|
26
|
+
RETRY_OPTIONS = {
|
|
27
|
+
max: RETRY_MAX,
|
|
28
|
+
interval: RETRY_INTERVAL,
|
|
29
|
+
backoff_factor: RETRY_BACKOFF_FACTOR,
|
|
30
|
+
interval_randomness: RETRY_INTERVAL_RANDOMNESS,
|
|
31
|
+
retry_statuses: RETRY_STATUSES,
|
|
32
|
+
# Faraday::TimeoutError is left out: a read timeout on a POST must not
|
|
33
|
+
# be resent (see PRE_SEND_ERRORS).
|
|
34
|
+
exceptions: [Faraday::ConnectionFailed, Faraday::RetriableResponse],
|
|
35
|
+
# Empty methods routes every retry decision through RETRY_IF.
|
|
36
|
+
methods: [],
|
|
37
|
+
retry_if: RETRY_IF
|
|
38
|
+
}.freeze
|
|
16
39
|
USER_AGENT = "digiwin_dsp/#{VERSION} (Faraday/#{Faraday::VERSION})".freeze
|
|
17
40
|
|
|
18
41
|
STATUS_ERROR_MAP = {
|
|
@@ -67,13 +90,7 @@ module DigiwinDsp
|
|
|
67
90
|
def connection
|
|
68
91
|
@connection ||= Faraday.new(url: connection_base_url, headers: default_headers) do |f|
|
|
69
92
|
f.request :json
|
|
70
|
-
f.request :retry,
|
|
71
|
-
max: RETRY_MAX,
|
|
72
|
-
interval: RETRY_INTERVAL,
|
|
73
|
-
backoff_factor: RETRY_BACKOFF_FACTOR,
|
|
74
|
-
interval_randomness: RETRY_INTERVAL_RANDOMNESS,
|
|
75
|
-
retry_statuses: RETRY_STATUSES,
|
|
76
|
-
methods: %i[get post put patch delete]
|
|
93
|
+
f.request :retry, RETRY_OPTIONS
|
|
77
94
|
# max_nesting caps deserialization depth so a hostile / malformed DSP
|
|
78
95
|
# response can't allocate unbounded memory (DoS guard on the parser).
|
|
79
96
|
f.response :json, content_type: /\bjson\z/, parser_options: { max_nesting: 50 }
|
|
@@ -114,8 +131,12 @@ module DigiwinDsp
|
|
|
114
131
|
klass.new(message, **error_attrs(status, body))
|
|
115
132
|
end
|
|
116
133
|
|
|
134
|
+
# Every DSP endpoint answers with a JSON object; anything else on a 2xx
|
|
135
|
+
# (IIS HTML error page, empty body) would otherwise surface downstream
|
|
136
|
+
# as NoMethodError, outside the DigiwinDsp::Error tree. The body itself
|
|
137
|
+
# stays out of the message (PII).
|
|
117
138
|
def inspect_envelope(body)
|
|
118
|
-
|
|
139
|
+
raise ServerError.new("DSP returned a non-JSON-object 2xx body (#{body.class})", http_status: 200) unless body.is_a?(Hash)
|
|
119
140
|
|
|
120
141
|
failure = detect_envelope_failure(body)
|
|
121
142
|
return body unless failure
|
|
@@ -128,7 +149,7 @@ module DigiwinDsp
|
|
|
128
149
|
# - DSPOOFFICIAL100: { srvver, std_data: { execution: { code, description }, response } }
|
|
129
150
|
# - DSPOOFFICIAL001-005: { Status, Message, response_detail }
|
|
130
151
|
def detect_envelope_failure(body)
|
|
131
|
-
if (exec = body
|
|
152
|
+
if (exec = execution_block(body))
|
|
132
153
|
return nil if exec["code"].to_s == "0"
|
|
133
154
|
|
|
134
155
|
{ message: exec["description"].to_s, code: exec["code"] }
|
|
@@ -142,6 +163,13 @@ module DigiwinDsp
|
|
|
142
163
|
end
|
|
143
164
|
end
|
|
144
165
|
|
|
166
|
+
# Hash-checked dig: a 2xx like {"std_data":"x"} must not raise TypeError.
|
|
167
|
+
def execution_block(body)
|
|
168
|
+
std_data = body["std_data"]
|
|
169
|
+
exec = std_data["execution"] if std_data.is_a?(Hash)
|
|
170
|
+
exec if exec.is_a?(Hash)
|
|
171
|
+
end
|
|
172
|
+
|
|
145
173
|
# Neither known envelope. If DSP ships a third shape, failures would
|
|
146
174
|
# otherwise pass through as "success" silently — leave a trace.
|
|
147
175
|
def warn_unknown_envelope(body)
|
|
@@ -21,9 +21,15 @@ module DigiwinDsp
|
|
|
21
21
|
production: "https://digiwindsp.digiwin.com/DSP/api/webhook"
|
|
22
22
|
}.freeze
|
|
23
23
|
|
|
24
|
-
attr_accessor :api_key, :platform_id, :
|
|
24
|
+
attr_accessor :api_key, :platform_id, :logger,
|
|
25
25
|
:timeout, :open_timeout, :allowed_hosts
|
|
26
26
|
attr_writer :base_url, :webhook_base_url
|
|
27
|
+
attr_reader :environment
|
|
28
|
+
|
|
29
|
+
# Accept strings too (e.g. `c.environment = Rails.env`), like the ENV path.
|
|
30
|
+
def environment=(value)
|
|
31
|
+
@environment = value&.to_sym
|
|
32
|
+
end
|
|
27
33
|
|
|
28
34
|
def initialize
|
|
29
35
|
@api_key = ENV["DIGIWIN_DSP_API_KEY"]
|
data/lib/digiwin_dsp/version.rb
CHANGED
|
@@ -26,8 +26,13 @@ module DigiwinDsp
|
|
|
26
26
|
def self.extract_request(hash)
|
|
27
27
|
raise ParseError, "envelope must be a JSON object" unless hash.is_a?(Hash)
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
# Walk by hand: Hash#dig raises TypeError when an intermediate level
|
|
30
|
+
# is a String/Array, which would escape the DigiwinDsp::Error tree
|
|
31
|
+
# (webhooks are unsigned, so anyone can send such a body).
|
|
32
|
+
request = %w[digi_body std_data parameter request].reduce(hash) do |node, key|
|
|
33
|
+
node.is_a?(Hash) ? node[key] : nil
|
|
34
|
+
end
|
|
35
|
+
request || raise(ParseError, "envelope missing digi_body.std_data.parameter.request")
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
private_class_method :parse_json, :extract_request
|
|
@@ -6,6 +6,13 @@ module DigiwinDsp
|
|
|
6
6
|
# See docs/dsp-specs/DSPOOFFICIAL100.yaml under
|
|
7
7
|
# "庫存數量更新 product/inventory_update".
|
|
8
8
|
class InventoryUpdate < Event
|
|
9
|
+
def self.parse(raw_body)
|
|
10
|
+
event = super
|
|
11
|
+
raise ParseError, "product/inventory_update payload must be a JSON object" unless event.request.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
event
|
|
14
|
+
end
|
|
15
|
+
|
|
9
16
|
def prod = request["prod"]
|
|
10
17
|
def platform_id = request["platform_id"]
|
|
11
18
|
def sale_page_id = request["sale_page_id"]
|
|
@@ -6,6 +6,13 @@ module DigiwinDsp
|
|
|
6
6
|
# delivered, etc.). See docs/dsp-specs/DSPOOFFICIAL100.yaml under
|
|
7
7
|
# "倉儲貨態更新 wms/logistics/package/update".
|
|
8
8
|
class LogisticsUpdate < Event
|
|
9
|
+
def self.parse(raw_body)
|
|
10
|
+
event = super
|
|
11
|
+
raise ParseError, "wms/logistics/package/update payload must be a JSON object" unless event.request.is_a?(Hash)
|
|
12
|
+
|
|
13
|
+
event
|
|
14
|
+
end
|
|
15
|
+
|
|
9
16
|
def form_no = request["form_no"]
|
|
10
17
|
def func_name = request["func_name"]
|
|
11
18
|
def status_date = request["status_date"]
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: digiwin_dsp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zac
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -101,7 +101,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
101
101
|
requirements:
|
|
102
102
|
- - ">="
|
|
103
103
|
- !ruby/object:Gem::Version
|
|
104
|
-
version: 3.
|
|
104
|
+
version: 3.3.0
|
|
105
105
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
106
106
|
requirements:
|
|
107
107
|
- - ">="
|