digiwin_dsp 0.1.1 → 0.2.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 +60 -2
- data/README.md +34 -18
- data/lib/digiwin_dsp/authenticator.rb +3 -1
- data/lib/digiwin_dsp/client.rb +38 -14
- data/lib/digiwin_dsp/configuration.rb +35 -13
- data/lib/digiwin_dsp/resources/cancellation.rb +5 -3
- data/lib/digiwin_dsp/resources/invoice.rb +5 -3
- data/lib/digiwin_dsp/resources/order.rb +5 -3
- data/lib/digiwin_dsp/resources/return.rb +5 -3
- data/lib/digiwin_dsp/version.rb +1 -1
- data/lib/digiwin_dsp.rb +12 -8
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce7484e03d7d931e7b16d56ac6be5c6842414da91b22a559d4d298aaa69c25e7
|
|
4
|
+
data.tar.gz: d250ea83a0f9516d49679311ab960e0009f3597ac46b553e6efdc0e344d280ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b496c0024aacf110d67fdb273d5f22b93c3825685dd0acedea7e235efa6cfd2aee4cb0cc247edf96272fe1de58a5da95ae233200b3cbb0dfc4f8333cc987fd8
|
|
7
|
+
data.tar.gz: 43641ec002ecd264abf8d3734234ea0bd66674608c8c499c74f5264a9d577181c0ba97664a9140ca3fa68a033e6055e368c2680ef7f3248ea199e36ffc7e2b08
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,63 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-05-21
|
|
10
|
+
|
|
11
|
+
Security + correctness release. Addresses every HIGH and MEDIUM finding
|
|
12
|
+
from the v0.1.x code review. Pre-1.0 SemVer; contains one breaking change.
|
|
13
|
+
|
|
14
|
+
### BREAKING
|
|
15
|
+
|
|
16
|
+
- **`DigiwinDsp::Error#response_body` removed.** Storing the full DSP
|
|
17
|
+
response on every exception leaked buyer PII (names, addresses, phone
|
|
18
|
+
numbers) to Sentry/Honeybadger/Rollbar via their default instance-var
|
|
19
|
+
serialization. Structured fields remain: `#code`, `#dsp_message`,
|
|
20
|
+
`#http_status`, `#request_id`. If you need the raw body, capture it in
|
|
21
|
+
your own Faraday middleware before the request reaches this gem.
|
|
22
|
+
**Migration:** delete any `e.response_body` access from rescue blocks.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `Configuration#allowed_hosts` (default `["digiwindsp.digiwin.com"]`) —
|
|
27
|
+
SSRF allowlist enforced on `#base_url`. Extend it for proxy/mock setups:
|
|
28
|
+
`c.allowed_hosts += ["dsp-proxy.your-co.internal"]`.
|
|
29
|
+
- CRLF (`\r` / `\n`) validation on `idempotency_key` and every entry of
|
|
30
|
+
the `headers:` kwarg in `Client#post`. Raises `ArgumentError` on
|
|
31
|
+
injection attempts (closes a header-smuggling vector that Faraday's
|
|
32
|
+
default adapter doesn't fully catch).
|
|
33
|
+
- `bundler-audit` ~> 0.9 in dev/test + a CI step (`bundle-audit check
|
|
34
|
+
--update`) that fails on any known CVE in the locked dependency tree.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- `Configuration#base_url` now validates the resolved URL:
|
|
39
|
+
- scheme MUST be `https` (no HTTP downgrade for the `DSP-api-key`)
|
|
40
|
+
- host MUST be in `allowed_hosts` (default: only `digiwindsp.digiwin.com`)
|
|
41
|
+
- malformed URIs raise `ConfigurationError`
|
|
42
|
+
- Resources::{Order,Cancellation,Invoice,Return}#create now raise
|
|
43
|
+
`DigiwinDsp::ServerError` when DSP returns `Status:"Success"` without a
|
|
44
|
+
`response_detail` key, instead of returning `nil` (which became a
|
|
45
|
+
silent `NoMethodError` downstream).
|
|
46
|
+
- Faraday retry now uses real exponential backoff with jitter
|
|
47
|
+
(`interval: 0.5, backoff_factor: 2, interval_randomness: 0.5`).
|
|
48
|
+
Previously `interval: 0, backoff_factor: 1` fired all three retries
|
|
49
|
+
instantly — actively counterproductive on 429 throttling.
|
|
50
|
+
- Faraday `:json` middleware gains `parser_options: { max_nesting: 50 }`
|
|
51
|
+
as a DoS guard against hostile / malformed DSP responses.
|
|
52
|
+
- `Resources::*.create` class-method shortcuts now declare typed kwargs
|
|
53
|
+
(`idempotency_key:`, `digi_header:`) so caller typos raise `ArgumentError`
|
|
54
|
+
at call time instead of being swallowed by `**`.
|
|
55
|
+
- README configuration table distinguishes runtime-used settings from
|
|
56
|
+
reserved ones; calls out that `platform_id` lives in `request_detail`,
|
|
57
|
+
not auth headers; documents `allowed_hosts` + the proxy-override pattern.
|
|
58
|
+
|
|
59
|
+
### Removed
|
|
60
|
+
|
|
61
|
+
- `DigiwinDsp::Authenticator#auth_headers` no longer calls
|
|
62
|
+
`Configuration#validate!`. Validation was redundant (Client#post runs
|
|
63
|
+
it per-request) and silently skipped after construction-time mutation
|
|
64
|
+
due to connection memoization.
|
|
65
|
+
|
|
9
66
|
## [0.1.1] - 2026-05-21
|
|
10
67
|
|
|
11
68
|
### Added
|
|
@@ -26,7 +83,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
26
83
|
|
|
27
84
|
## [0.1.0] - 2026-05-21
|
|
28
85
|
|
|
29
|
-
Initial release. Covers the four 自有官網模組 endpoints under `/v1/SalesOrder/*`.
|
|
86
|
+
Initial release. Covers the four Self-hosted Website Module (自有官網模組) endpoints under `/v1/SalesOrder/*`.
|
|
30
87
|
|
|
31
88
|
### Added
|
|
32
89
|
|
|
@@ -61,6 +118,7 @@ Initial release. Covers the four 自有官網模組 endpoints under `/v1/SalesOr
|
|
|
61
118
|
- The gem is **synchronous on purpose**. Callers wrap requests in their own background job runner (e.g. ActiveJob) when needed.
|
|
62
119
|
- Idempotency: clients can send `X-Idempotency-Key` via the `idempotency_key:` kwarg. DSP also dedupes server-side by `form_no + platform_id`.
|
|
63
120
|
|
|
64
|
-
[Unreleased]: https://github.com/7a6163/digiwin_dsp/compare/v0.
|
|
121
|
+
[Unreleased]: https://github.com/7a6163/digiwin_dsp/compare/v0.2.0...HEAD
|
|
122
|
+
[0.2.0]: https://github.com/7a6163/digiwin_dsp/compare/v0.1.1...v0.2.0
|
|
65
123
|
[0.1.1]: https://github.com/7a6163/digiwin_dsp/compare/v0.1.0...v0.1.1
|
|
66
124
|
[0.1.0]: https://github.com/7a6163/digiwin_dsp/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
[](https://www.ruby-lang.org/)
|
|
6
6
|
[](./LICENSE.txt)
|
|
7
7
|
|
|
8
|
-
Ruby client for the Digiwin DSP
|
|
8
|
+
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
9
|
|
|
10
10
|
| Operation | Resource | Endpoint |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
12
|
+
| Create order | `DigiwinDsp::Resources::Order` | `POST /v1/SalesOrder/add` |
|
|
13
|
+
| Cancel order | `DigiwinDsp::Resources::Cancellation` | `POST /v1/SalesOrder/cancel` |
|
|
14
|
+
| Invoice update | `DigiwinDsp::Resources::Invoice` | `POST /v1/SalesOrder/invoice` |
|
|
15
|
+
| Return | `DigiwinDsp::Resources::Return` | `POST /v1/SalesOrder/return` |
|
|
16
16
|
|
|
17
17
|
See [`docs/dsp-api-spec.md`](./docs/dsp-api-spec.md) (plus `docs/dsp-specs/*.yaml`) for the wire spec.
|
|
18
18
|
|
|
@@ -46,16 +46,17 @@ end
|
|
|
46
46
|
|
|
47
47
|
Every setting also falls back to an ENV var:
|
|
48
48
|
|
|
49
|
-
| Setting | ENV var | Default |
|
|
50
|
-
|
|
51
|
-
| `api_key` | `DIGIWIN_DSP_API_KEY` | _(required)_ |
|
|
52
|
-
| `api_secret` | `DIGIWIN_DSP_API_SECRET` | `nil` |
|
|
53
|
-
| `platform_id` | `DIGIWIN_DSP_PLATFORM_ID` | `nil` |
|
|
54
|
-
| `environment` | `DIGIWIN_DSP_ENV` | `:sandbox` |
|
|
55
|
-
| `base_url` | `DIGIWIN_DSP_BASE_URL` | resolved from `environment` |
|
|
56
|
-
| `
|
|
57
|
-
| `
|
|
58
|
-
| `
|
|
49
|
+
| Setting | ENV var | Default | Notes |
|
|
50
|
+
|---|---|---|---|
|
|
51
|
+
| `api_key` | `DIGIWIN_DSP_API_KEY` | _(required)_ | sent as `DSP-api-key` header |
|
|
52
|
+
| `api_secret` | `DIGIWIN_DSP_API_SECRET` | `nil` | reserved for future HMAC signing; unused today |
|
|
53
|
+
| `platform_id` | `DIGIWIN_DSP_PLATFORM_ID` | `nil` | sent **per-record** in `request_detail.platform_id` (not in auth headers) |
|
|
54
|
+
| `environment` | `DIGIWIN_DSP_ENV` | `:sandbox` | `:sandbox` (UAT) or `:production` |
|
|
55
|
+
| `base_url` | `DIGIWIN_DSP_BASE_URL` | resolved from `environment` | must be `https://` and have a host in `allowed_hosts` |
|
|
56
|
+
| `allowed_hosts` | — | `["digiwindsp.digiwin.com"]` | SSRF allowlist; extend if you proxy DSP through a different domain |
|
|
57
|
+
| `timeout` | — | `10` | seconds |
|
|
58
|
+
| `open_timeout` | — | `5` | seconds |
|
|
59
|
+
| `logger` | — | `Logger.new(IO::NULL)` | any Logger-like object |
|
|
59
60
|
|
|
60
61
|
Base URLs (resolved from `environment`):
|
|
61
62
|
|
|
@@ -64,6 +65,21 @@ Base URLs (resolved from `environment`):
|
|
|
64
65
|
|
|
65
66
|
See [`.env.local.example`](./.env.local.example) for a starter env file.
|
|
66
67
|
|
|
68
|
+
### Custom proxy host
|
|
69
|
+
|
|
70
|
+
If you front DSP with a corporate proxy or use a mock server, add the host:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
DigiwinDsp.configure do |c|
|
|
74
|
+
c.allowed_hosts += ["dsp-proxy.your-co.internal"]
|
|
75
|
+
c.base_url = "https://dsp-proxy.your-co.internal/api/DSP"
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Any host not in `allowed_hosts`, or any non-`https://` URL, raises
|
|
80
|
+
`DigiwinDsp::ConfigurationError`. This is an SSRF + HTTP-downgrade guard
|
|
81
|
+
since `DIGIWIN_DSP_BASE_URL` accepts arbitrary input.
|
|
82
|
+
|
|
67
83
|
## Usage
|
|
68
84
|
|
|
69
85
|
Each resource exposes a single `#create(records, idempotency_key:, digi_header:)` method that returns the parsed `response_detail` array on success or raises a typed exception on failure.
|
|
@@ -75,7 +91,7 @@ record = {
|
|
|
75
91
|
"platform_id" => "acme_storefront_test",
|
|
76
92
|
"create_datetime" => Time.now.strftime("%Y-%m-%d %H:%M:%S"),
|
|
77
93
|
"site_no" => "acme_storefront_test",
|
|
78
|
-
"form_no" => "WEB202605200001", #
|
|
94
|
+
"form_no" => "WEB202605200001", # storefront order number
|
|
79
95
|
"order_date" => "20260520",
|
|
80
96
|
"buyer_name" => "王小明",
|
|
81
97
|
"receiver_name" => "王小明",
|
|
@@ -92,7 +108,7 @@ record = {
|
|
|
92
108
|
"price" => "100",
|
|
93
109
|
"subtotal" => "100",
|
|
94
110
|
"payment" => "100",
|
|
95
|
-
"order_status" => "3", # 3 =
|
|
111
|
+
"order_status" => "3", # 3 = new order
|
|
96
112
|
"last_record" => "Y" # "Y" on the final line
|
|
97
113
|
}
|
|
98
114
|
|
|
@@ -151,7 +167,7 @@ end
|
|
|
151
167
|
|
|
152
168
|
## Error handling
|
|
153
169
|
|
|
154
|
-
All exceptions inherit from `DigiwinDsp::Error` and carry
|
|
170
|
+
All exceptions inherit from `DigiwinDsp::Error` and carry structured attributes safe for logging: `#code`, `#dsp_message`, `#http_status`, `#request_id`. The raw response body is **intentionally not exposed** on exceptions to prevent PII leakage to error reporters like Sentry/Rollbar that serialize exception instance variables.
|
|
155
171
|
|
|
156
172
|
| Exception | Raised when |
|
|
157
173
|
|---|---|
|
|
@@ -8,8 +8,10 @@ module DigiwinDsp
|
|
|
8
8
|
@configuration = configuration
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# Validation lives in Client#post (called per-request). Authenticator
|
|
12
|
+
# only ran validate! once per connection lifetime due to Client's
|
|
13
|
+
# memoized #default_headers, so the redundant call was misleading.
|
|
11
14
|
def auth_headers
|
|
12
|
-
@configuration.validate!
|
|
13
15
|
{ HEADER_NAME => @configuration.api_key }
|
|
14
16
|
end
|
|
15
17
|
end
|
data/lib/digiwin_dsp/client.rb
CHANGED
|
@@ -6,6 +6,13 @@ require "faraday/retry"
|
|
|
6
6
|
module DigiwinDsp
|
|
7
7
|
class Client
|
|
8
8
|
RETRY_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
9
|
+
# Exponential backoff: ~0.5s, ~1s, ~2s between attempts, ±50% jitter so
|
|
10
|
+
# multiple Rails workers retrying the same upstream blip don't synchronize
|
|
11
|
+
# into a thundering herd against DSP.
|
|
12
|
+
RETRY_MAX = 3
|
|
13
|
+
RETRY_INTERVAL = 0.5
|
|
14
|
+
RETRY_BACKOFF_FACTOR = 2
|
|
15
|
+
RETRY_INTERVAL_RANDOMNESS = 0.5
|
|
9
16
|
USER_AGENT = "digiwin_dsp/#{VERSION} (Faraday/#{Faraday::VERSION})".freeze
|
|
10
17
|
|
|
11
18
|
STATUS_ERROR_MAP = {
|
|
@@ -16,13 +23,15 @@ module DigiwinDsp
|
|
|
16
23
|
429 => RateLimitError
|
|
17
24
|
}.freeze
|
|
18
25
|
|
|
19
|
-
#
|
|
26
|
+
# Patterns DSP returns in the response body's `Message` field on Status=Failure.
|
|
27
|
+
# The Chinese substrings are verbatim DSP responses — see docs/dsp-api-spec.md.
|
|
28
|
+
# Order matters: more-specific patterns first.
|
|
20
29
|
ENVELOPE_FAILURE_MAP = [
|
|
21
|
-
[/\ADuplicated:/, DuplicateRequestError],
|
|
22
|
-
[/\AProcessing:資料處理中/, RateLimitError],
|
|
23
|
-
[/\AProcessing:取消訂單處理中/, ValidationError],
|
|
24
|
-
[/\AWrongStatus:/, ValidationError],
|
|
25
|
-
[/\A系統異常:/, ServerError]
|
|
30
|
+
[/\ADuplicated:/, DuplicateRequestError], # order already exists
|
|
31
|
+
[/\AProcessing:資料處理中/, RateLimitError], # transient; retry later
|
|
32
|
+
[/\AProcessing:取消訂單處理中/, ValidationError], # cancel in flight
|
|
33
|
+
[/\AWrongStatus:/, ValidationError], # bad payload
|
|
34
|
+
[/\A系統異常:/, ServerError] # DSP internal error
|
|
26
35
|
].freeze
|
|
27
36
|
|
|
28
37
|
def initialize(configuration: DigiwinDsp.configuration, authenticator: nil)
|
|
@@ -32,6 +41,7 @@ module DigiwinDsp
|
|
|
32
41
|
|
|
33
42
|
def post(path, body, idempotency_key: nil, headers: {})
|
|
34
43
|
@configuration.validate!
|
|
44
|
+
sanitize_request_headers!(idempotency_key, headers)
|
|
35
45
|
response = connection.post(normalize_path(path)) do |req|
|
|
36
46
|
req.headers["X-Idempotency-Key"] = idempotency_key if idempotency_key
|
|
37
47
|
headers.each { |k, v| req.headers[k] = v }
|
|
@@ -48,12 +58,15 @@ module DigiwinDsp
|
|
|
48
58
|
@connection ||= Faraday.new(url: connection_base_url, headers: default_headers) do |f|
|
|
49
59
|
f.request :json
|
|
50
60
|
f.request :retry,
|
|
51
|
-
max:
|
|
52
|
-
interval:
|
|
53
|
-
backoff_factor:
|
|
61
|
+
max: RETRY_MAX,
|
|
62
|
+
interval: RETRY_INTERVAL,
|
|
63
|
+
backoff_factor: RETRY_BACKOFF_FACTOR,
|
|
64
|
+
interval_randomness: RETRY_INTERVAL_RANDOMNESS,
|
|
54
65
|
retry_statuses: RETRY_STATUSES,
|
|
55
66
|
methods: %i[get post put patch delete]
|
|
56
|
-
|
|
67
|
+
# max_nesting caps deserialization depth so a hostile / malformed DSP
|
|
68
|
+
# response can't allocate unbounded memory (DoS guard on the parser).
|
|
69
|
+
f.response :json, content_type: /\bjson\z/, parser_options: { max_nesting: 50 }
|
|
57
70
|
f.response :logger, @configuration.logger, headers: false, bodies: false, log_level: :debug
|
|
58
71
|
f.adapter Faraday.default_adapter
|
|
59
72
|
f.options.timeout = @configuration.timeout
|
|
@@ -110,8 +123,7 @@ module DigiwinDsp
|
|
|
110
123
|
code: hash["error_code"] || hash["code"],
|
|
111
124
|
dsp_message: hash["error_message"] || hash["message"],
|
|
112
125
|
request_id: hash["request_id"],
|
|
113
|
-
http_status: status
|
|
114
|
-
response_body: body
|
|
126
|
+
http_status: status
|
|
115
127
|
}
|
|
116
128
|
end
|
|
117
129
|
|
|
@@ -120,9 +132,21 @@ module DigiwinDsp
|
|
|
120
132
|
code: body["Status"],
|
|
121
133
|
dsp_message: body["Message"],
|
|
122
134
|
request_id: body["request_id"],
|
|
123
|
-
http_status: 200
|
|
124
|
-
response_body: body
|
|
135
|
+
http_status: 200
|
|
125
136
|
}
|
|
126
137
|
end
|
|
138
|
+
|
|
139
|
+
# Block CRLF/LF/CR in header names and values. RFC 7230 §3.2.4 forbids
|
|
140
|
+
# them and Faraday + Net::HTTP catch many forms, but not all — close
|
|
141
|
+
# the gap to prevent header-injection / request-smuggling.
|
|
142
|
+
def sanitize_request_headers!(idempotency_key, headers)
|
|
143
|
+
sanitize_header!("X-Idempotency-Key", idempotency_key) if idempotency_key
|
|
144
|
+
headers.each { |k, v| sanitize_header!(k, v) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def sanitize_header!(name, value)
|
|
148
|
+
raise ArgumentError, "invalid header value for #{name.inspect}: contains CRLF/LF/CR" if value.to_s.match?(/[\r\n]/)
|
|
149
|
+
raise ArgumentError, "invalid header name #{name.inspect}: contains CRLF/LF/CR" if name.to_s.match?(/[\r\n]/)
|
|
150
|
+
end
|
|
127
151
|
end
|
|
128
152
|
end
|
|
@@ -1,45 +1,67 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "logger"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
module DigiwinDsp
|
|
6
7
|
class Configuration
|
|
7
8
|
DEFAULT_TIMEOUT = 10
|
|
8
9
|
DEFAULT_OPEN_TIMEOUT = 5
|
|
10
|
+
DEFAULT_ALLOWED_HOSTS = ["digiwindsp.digiwin.com"].freeze
|
|
9
11
|
|
|
10
12
|
BASE_URLS = {
|
|
11
13
|
sandbox: "https://digiwindsp.digiwin.com/DSP_UAT/api/DSP",
|
|
12
14
|
production: "https://digiwindsp.digiwin.com/DSP/api/DSP"
|
|
13
15
|
}.freeze
|
|
14
16
|
|
|
15
|
-
attr_accessor :api_key, :api_secret, :platform_id, :environment, :logger,
|
|
17
|
+
attr_accessor :api_key, :api_secret, :platform_id, :environment, :logger,
|
|
18
|
+
:timeout, :open_timeout, :allowed_hosts
|
|
16
19
|
attr_writer :base_url
|
|
17
20
|
|
|
18
21
|
def initialize
|
|
19
|
-
@api_key
|
|
20
|
-
@api_secret
|
|
21
|
-
@platform_id
|
|
22
|
-
@environment
|
|
23
|
-
@base_url
|
|
24
|
-
@timeout
|
|
25
|
-
@open_timeout
|
|
26
|
-
@logger
|
|
22
|
+
@api_key = ENV["DIGIWIN_DSP_API_KEY"]
|
|
23
|
+
@api_secret = ENV["DIGIWIN_DSP_API_SECRET"]
|
|
24
|
+
@platform_id = ENV["DIGIWIN_DSP_PLATFORM_ID"]
|
|
25
|
+
@environment = ENV.fetch("DIGIWIN_DSP_ENV") { "sandbox" }.to_sym
|
|
26
|
+
@base_url = ENV["DIGIWIN_DSP_BASE_URL"]
|
|
27
|
+
@timeout = DEFAULT_TIMEOUT
|
|
28
|
+
@open_timeout = DEFAULT_OPEN_TIMEOUT
|
|
29
|
+
@logger = Logger.new(IO::NULL)
|
|
30
|
+
@allowed_hosts = DEFAULT_ALLOWED_HOSTS.dup
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
def base_url
|
|
30
|
-
|
|
34
|
+
url = @base_url || resolve_default_base_url
|
|
35
|
+
validate_url!(url)
|
|
36
|
+
url
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def validate!
|
|
40
|
+
return unless api_key.nil? || api_key.to_s.empty?
|
|
41
|
+
|
|
42
|
+
raise ConfigurationError,
|
|
43
|
+
"api_key is required (set via DigiwinDsp.configure or DIGIWIN_DSP_API_KEY)"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
31
47
|
|
|
48
|
+
def resolve_default_base_url
|
|
32
49
|
BASE_URLS.fetch(environment) do
|
|
33
50
|
raise ConfigurationError,
|
|
34
51
|
"unknown environment #{environment.inspect}; expected one of #{BASE_URLS.keys.inspect}"
|
|
35
52
|
end
|
|
36
53
|
end
|
|
37
54
|
|
|
38
|
-
def
|
|
39
|
-
|
|
55
|
+
def validate_url!(url)
|
|
56
|
+
uri = URI.parse(url)
|
|
57
|
+
raise ConfigurationError, "base_url must use https (got #{uri.scheme.inspect})" unless uri.scheme == "https"
|
|
58
|
+
return if allowed_hosts.include?(uri.host)
|
|
40
59
|
|
|
41
60
|
raise ConfigurationError,
|
|
42
|
-
"
|
|
61
|
+
"base_url host #{uri.host.inspect} is not in allowed_hosts #{allowed_hosts.inspect}; " \
|
|
62
|
+
"add it via DigiwinDsp.configure { |c| c.allowed_hosts += [host] }"
|
|
63
|
+
rescue URI::InvalidURIError => e
|
|
64
|
+
raise ConfigurationError, "base_url is not a valid URI: #{e.message}"
|
|
43
65
|
end
|
|
44
66
|
end
|
|
45
67
|
end
|
|
@@ -5,8 +5,8 @@ module DigiwinDsp
|
|
|
5
5
|
class Cancellation
|
|
6
6
|
PATH = "/v1/SalesOrder/cancel"
|
|
7
7
|
|
|
8
|
-
def self.create(records,
|
|
9
|
-
new.create(records,
|
|
8
|
+
def self.create(records, idempotency_key: nil, digi_header: nil)
|
|
9
|
+
new.create(records, idempotency_key: idempotency_key, digi_header: digi_header)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def initialize(client = Client.new)
|
|
@@ -16,7 +16,9 @@ module DigiwinDsp
|
|
|
16
16
|
def create(records, idempotency_key: nil, digi_header: nil)
|
|
17
17
|
body = Serializers::CancellationSerializer.serialize(records, digi_header: digi_header)
|
|
18
18
|
response = @client.post(PATH, body, idempotency_key: idempotency_key)
|
|
19
|
-
response
|
|
19
|
+
response.fetch("response_detail") do
|
|
20
|
+
raise DigiwinDsp::ServerError, "DSP returned Status=Success without response_detail"
|
|
21
|
+
end
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -5,8 +5,8 @@ module DigiwinDsp
|
|
|
5
5
|
class Invoice
|
|
6
6
|
PATH = "/v1/SalesOrder/invoice"
|
|
7
7
|
|
|
8
|
-
def self.create(records,
|
|
9
|
-
new.create(records,
|
|
8
|
+
def self.create(records, idempotency_key: nil, digi_header: nil)
|
|
9
|
+
new.create(records, idempotency_key: idempotency_key, digi_header: digi_header)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def initialize(client = Client.new)
|
|
@@ -16,7 +16,9 @@ module DigiwinDsp
|
|
|
16
16
|
def create(records, idempotency_key: nil, digi_header: nil)
|
|
17
17
|
body = Serializers::InvoiceSerializer.serialize(records, digi_header: digi_header)
|
|
18
18
|
response = @client.post(PATH, body, idempotency_key: idempotency_key)
|
|
19
|
-
response
|
|
19
|
+
response.fetch("response_detail") do
|
|
20
|
+
raise DigiwinDsp::ServerError, "DSP returned Status=Success without response_detail"
|
|
21
|
+
end
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -5,8 +5,8 @@ module DigiwinDsp
|
|
|
5
5
|
class Order
|
|
6
6
|
PATH = "/v1/SalesOrder/add"
|
|
7
7
|
|
|
8
|
-
def self.create(records,
|
|
9
|
-
new.create(records,
|
|
8
|
+
def self.create(records, idempotency_key: nil, digi_header: nil)
|
|
9
|
+
new.create(records, idempotency_key: idempotency_key, digi_header: digi_header)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def initialize(client = Client.new)
|
|
@@ -16,7 +16,9 @@ module DigiwinDsp
|
|
|
16
16
|
def create(records, idempotency_key: nil, digi_header: nil)
|
|
17
17
|
body = Serializers::SalesOrderSerializer.serialize(records, digi_header: digi_header)
|
|
18
18
|
response = @client.post(PATH, body, idempotency_key: idempotency_key)
|
|
19
|
-
response
|
|
19
|
+
response.fetch("response_detail") do
|
|
20
|
+
raise DigiwinDsp::ServerError, "DSP returned Status=Success without response_detail"
|
|
21
|
+
end
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
end
|
|
@@ -5,8 +5,8 @@ module DigiwinDsp
|
|
|
5
5
|
class Return
|
|
6
6
|
PATH = "/v1/SalesOrder/return"
|
|
7
7
|
|
|
8
|
-
def self.create(records,
|
|
9
|
-
new.create(records,
|
|
8
|
+
def self.create(records, idempotency_key: nil, digi_header: nil)
|
|
9
|
+
new.create(records, idempotency_key: idempotency_key, digi_header: digi_header)
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def initialize(client = Client.new)
|
|
@@ -16,7 +16,9 @@ module DigiwinDsp
|
|
|
16
16
|
def create(records, idempotency_key: nil, digi_header: nil)
|
|
17
17
|
body = Serializers::ReturnSerializer.serialize(records, digi_header: digi_header)
|
|
18
18
|
response = @client.post(PATH, body, idempotency_key: idempotency_key)
|
|
19
|
-
response
|
|
19
|
+
response.fetch("response_detail") do
|
|
20
|
+
raise DigiwinDsp::ServerError, "DSP returned Status=Success without response_detail"
|
|
21
|
+
end
|
|
20
22
|
end
|
|
21
23
|
end
|
|
22
24
|
end
|
data/lib/digiwin_dsp/version.rb
CHANGED
data/lib/digiwin_dsp.rb
CHANGED
|
@@ -4,15 +4,19 @@ require "zeitwerk"
|
|
|
4
4
|
|
|
5
5
|
module DigiwinDsp
|
|
6
6
|
class Error < StandardError
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
# NOTE: response_body is intentionally NOT exposed.
|
|
8
|
+
# Exception reporters (Sentry, Honeybadger, Rollbar, etc.) serialize
|
|
9
|
+
# instance variables by default; storing the raw DSP body would leak
|
|
10
|
+
# buyer PII (names, addresses, phone numbers) into third-party logging.
|
|
11
|
+
# Use the structured fields below for safe logging.
|
|
12
|
+
attr_reader :code, :dsp_message, :request_id, :http_status
|
|
13
|
+
|
|
14
|
+
def initialize(message = nil, code: nil, dsp_message: nil, request_id: nil, http_status: nil)
|
|
10
15
|
super(message)
|
|
11
|
-
@code
|
|
12
|
-
@dsp_message
|
|
13
|
-
@request_id
|
|
14
|
-
@http_status
|
|
15
|
-
@response_body = response_body
|
|
16
|
+
@code = code
|
|
17
|
+
@dsp_message = dsp_message
|
|
18
|
+
@request_id = request_id
|
|
19
|
+
@http_status = http_status
|
|
16
20
|
end
|
|
17
21
|
end
|
|
18
22
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: digiwin_dsp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zac
|
|
@@ -52,8 +52,9 @@ dependencies:
|
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
54
|
version: '2.6'
|
|
55
|
-
description: Synchronous Ruby gem wrapping the Digiwin DSP
|
|
56
|
-
order, cancel order, invoice update, return
|
|
55
|
+
description: Synchronous Ruby gem wrapping the Digiwin DSP Self-hosted Website Module
|
|
56
|
+
(自有官網模組) endpoints — create order, cancel order, invoice update, and return — for
|
|
57
|
+
use from a Rails storefront.
|
|
57
58
|
email:
|
|
58
59
|
- 579103+7a6163@users.noreply.github.com
|
|
59
60
|
executables: []
|
|
@@ -102,5 +103,5 @@ requirements: []
|
|
|
102
103
|
rubygems_version: 3.5.22
|
|
103
104
|
signing_key:
|
|
104
105
|
specification_version: 4
|
|
105
|
-
summary: Ruby client for the Digiwin DSP 自有官網模組 API.
|
|
106
|
+
summary: Ruby client for the Digiwin DSP Self-hosted Website Module (自有官網模組) API.
|
|
106
107
|
test_files: []
|