digiwin_dsp 0.1.1 → 0.2.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/CHANGELOG.md +83 -2
- data/README.md +34 -18
- data/lib/digiwin_dsp/authenticator.rb +3 -1
- data/lib/digiwin_dsp/client.rb +40 -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: cf609190d9515001cb6540d2bbf8a88c8e1f6a5294ea21edbe1408bad4305fc4
|
|
4
|
+
data.tar.gz: 61f86d3485d0322c3bd0af81bddcdf0ae3105327d9de63a038dbab6854a3901b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 977f1b3a3e31e5de38b1bc48327ef5427cbedeb12b60949f1812c964a768fc96ab1810ca4b1aae7f4427c38b85c6fe108f296082ec0b202052bab76578901a31
|
|
7
|
+
data.tar.gz: 9301f03f1af6630147a6d1ad88fd7db41fdd70c329886b5b526a75bbf4f2492643852f353115f5c65e22c759656d1cb86708620d28199f3d9b4514360f69da90
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,85 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.1] - 2026-05-21
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Envelope failure-message regexes now match anywhere in the string**
|
|
14
|
+
(dropped the `\A` anchor). Discovered via live UAT smoke test: DSP
|
|
15
|
+
prepends the offending `form_no` to `Message`, so the actual response
|
|
16
|
+
is e.g. `"ORDER-123:Duplicated:訂單不可重複"` rather than the
|
|
17
|
+
OpenAPI-example shape `"Duplicated:訂單不可重複"`. Previously the
|
|
18
|
+
prefix caused `DuplicateRequestError` / `RateLimitError` /
|
|
19
|
+
`ValidationError` / `ServerError` classifications to fall through to
|
|
20
|
+
generic `DigiwinDsp::Error`, defeating typed-rescue logic in callers.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- CI CVE audit switched from the `bundler-audit` Ruby gem to the
|
|
25
|
+
`7a6163/gem-audit-action@v1` GitHub Action (which wraps the
|
|
26
|
+
`gem-audit` Rust binary). Faster, no runtime gem to keep updated, and
|
|
27
|
+
the action handles platform/version selection. Dropped `bundler-audit`
|
|
28
|
+
from the dev/test Gemfile group.
|
|
29
|
+
- Rubocop now excludes `scripts/**/*` (operator scripts; not gem source).
|
|
30
|
+
|
|
31
|
+
## [0.2.0] - 2026-05-21
|
|
32
|
+
|
|
33
|
+
Security + correctness release. Addresses every HIGH and MEDIUM finding
|
|
34
|
+
from the v0.1.x code review. Pre-1.0 SemVer; contains one breaking change.
|
|
35
|
+
|
|
36
|
+
### BREAKING
|
|
37
|
+
|
|
38
|
+
- **`DigiwinDsp::Error#response_body` removed.** Storing the full DSP
|
|
39
|
+
response on every exception leaked buyer PII (names, addresses, phone
|
|
40
|
+
numbers) to Sentry/Honeybadger/Rollbar via their default instance-var
|
|
41
|
+
serialization. Structured fields remain: `#code`, `#dsp_message`,
|
|
42
|
+
`#http_status`, `#request_id`. If you need the raw body, capture it in
|
|
43
|
+
your own Faraday middleware before the request reaches this gem.
|
|
44
|
+
**Migration:** delete any `e.response_body` access from rescue blocks.
|
|
45
|
+
|
|
46
|
+
### Added
|
|
47
|
+
|
|
48
|
+
- `Configuration#allowed_hosts` (default `["digiwindsp.digiwin.com"]`) —
|
|
49
|
+
SSRF allowlist enforced on `#base_url`. Extend it for proxy/mock setups:
|
|
50
|
+
`c.allowed_hosts += ["dsp-proxy.your-co.internal"]`.
|
|
51
|
+
- CRLF (`\r` / `\n`) validation on `idempotency_key` and every entry of
|
|
52
|
+
the `headers:` kwarg in `Client#post`. Raises `ArgumentError` on
|
|
53
|
+
injection attempts (closes a header-smuggling vector that Faraday's
|
|
54
|
+
default adapter doesn't fully catch).
|
|
55
|
+
- `bundler-audit` ~> 0.9 in dev/test + a CI step (`bundle-audit check
|
|
56
|
+
--update`) that fails on any known CVE in the locked dependency tree.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- `Configuration#base_url` now validates the resolved URL:
|
|
61
|
+
- scheme MUST be `https` (no HTTP downgrade for the `DSP-api-key`)
|
|
62
|
+
- host MUST be in `allowed_hosts` (default: only `digiwindsp.digiwin.com`)
|
|
63
|
+
- malformed URIs raise `ConfigurationError`
|
|
64
|
+
- Resources::{Order,Cancellation,Invoice,Return}#create now raise
|
|
65
|
+
`DigiwinDsp::ServerError` when DSP returns `Status:"Success"` without a
|
|
66
|
+
`response_detail` key, instead of returning `nil` (which became a
|
|
67
|
+
silent `NoMethodError` downstream).
|
|
68
|
+
- Faraday retry now uses real exponential backoff with jitter
|
|
69
|
+
(`interval: 0.5, backoff_factor: 2, interval_randomness: 0.5`).
|
|
70
|
+
Previously `interval: 0, backoff_factor: 1` fired all three retries
|
|
71
|
+
instantly — actively counterproductive on 429 throttling.
|
|
72
|
+
- Faraday `:json` middleware gains `parser_options: { max_nesting: 50 }`
|
|
73
|
+
as a DoS guard against hostile / malformed DSP responses.
|
|
74
|
+
- `Resources::*.create` class-method shortcuts now declare typed kwargs
|
|
75
|
+
(`idempotency_key:`, `digi_header:`) so caller typos raise `ArgumentError`
|
|
76
|
+
at call time instead of being swallowed by `**`.
|
|
77
|
+
- README configuration table distinguishes runtime-used settings from
|
|
78
|
+
reserved ones; calls out that `platform_id` lives in `request_detail`,
|
|
79
|
+
not auth headers; documents `allowed_hosts` + the proxy-override pattern.
|
|
80
|
+
|
|
81
|
+
### Removed
|
|
82
|
+
|
|
83
|
+
- `DigiwinDsp::Authenticator#auth_headers` no longer calls
|
|
84
|
+
`Configuration#validate!`. Validation was redundant (Client#post runs
|
|
85
|
+
it per-request) and silently skipped after construction-time mutation
|
|
86
|
+
due to connection memoization.
|
|
87
|
+
|
|
9
88
|
## [0.1.1] - 2026-05-21
|
|
10
89
|
|
|
11
90
|
### Added
|
|
@@ -26,7 +105,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
26
105
|
|
|
27
106
|
## [0.1.0] - 2026-05-21
|
|
28
107
|
|
|
29
|
-
Initial release. Covers the four 自有官網模組 endpoints under `/v1/SalesOrder/*`.
|
|
108
|
+
Initial release. Covers the four Self-hosted Website Module (自有官網模組) endpoints under `/v1/SalesOrder/*`.
|
|
30
109
|
|
|
31
110
|
### Added
|
|
32
111
|
|
|
@@ -61,6 +140,8 @@ Initial release. Covers the four 自有官網模組 endpoints under `/v1/SalesOr
|
|
|
61
140
|
- The gem is **synchronous on purpose**. Callers wrap requests in their own background job runner (e.g. ActiveJob) when needed.
|
|
62
141
|
- Idempotency: clients can send `X-Idempotency-Key` via the `idempotency_key:` kwarg. DSP also dedupes server-side by `form_no + platform_id`.
|
|
63
142
|
|
|
64
|
-
[Unreleased]: https://github.com/7a6163/digiwin_dsp/compare/v0.
|
|
143
|
+
[Unreleased]: https://github.com/7a6163/digiwin_dsp/compare/v0.2.1...HEAD
|
|
144
|
+
[0.2.1]: https://github.com/7a6163/digiwin_dsp/compare/v0.2.0...v0.2.1
|
|
145
|
+
[0.2.0]: https://github.com/7a6163/digiwin_dsp/compare/v0.1.1...v0.2.0
|
|
65
146
|
[0.1.1]: https://github.com/7a6163/digiwin_dsp/compare/v0.1.0...v0.1.1
|
|
66
147
|
[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,17 @@ 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
|
+
# Live DSP prepends the offending form_no to Message (e.g.
|
|
29
|
+
# "ORDER-123:Duplicated:訂單不可重複"), so we substring-match rather than
|
|
30
|
+
# anchor with \A. Order matters: more-specific patterns first.
|
|
20
31
|
ENVELOPE_FAILURE_MAP = [
|
|
21
|
-
[
|
|
22
|
-
[
|
|
23
|
-
[
|
|
24
|
-
[
|
|
25
|
-
[
|
|
32
|
+
[/Duplicated:/, DuplicateRequestError], # order already exists
|
|
33
|
+
[/Processing:資料處理中/, RateLimitError], # transient; retry later
|
|
34
|
+
[/Processing:取消訂單處理中/, ValidationError], # cancel in flight
|
|
35
|
+
[/WrongStatus:/, ValidationError], # bad payload
|
|
36
|
+
[/系統異常:/, ServerError] # DSP internal error
|
|
26
37
|
].freeze
|
|
27
38
|
|
|
28
39
|
def initialize(configuration: DigiwinDsp.configuration, authenticator: nil)
|
|
@@ -32,6 +43,7 @@ module DigiwinDsp
|
|
|
32
43
|
|
|
33
44
|
def post(path, body, idempotency_key: nil, headers: {})
|
|
34
45
|
@configuration.validate!
|
|
46
|
+
sanitize_request_headers!(idempotency_key, headers)
|
|
35
47
|
response = connection.post(normalize_path(path)) do |req|
|
|
36
48
|
req.headers["X-Idempotency-Key"] = idempotency_key if idempotency_key
|
|
37
49
|
headers.each { |k, v| req.headers[k] = v }
|
|
@@ -48,12 +60,15 @@ module DigiwinDsp
|
|
|
48
60
|
@connection ||= Faraday.new(url: connection_base_url, headers: default_headers) do |f|
|
|
49
61
|
f.request :json
|
|
50
62
|
f.request :retry,
|
|
51
|
-
max:
|
|
52
|
-
interval:
|
|
53
|
-
backoff_factor:
|
|
63
|
+
max: RETRY_MAX,
|
|
64
|
+
interval: RETRY_INTERVAL,
|
|
65
|
+
backoff_factor: RETRY_BACKOFF_FACTOR,
|
|
66
|
+
interval_randomness: RETRY_INTERVAL_RANDOMNESS,
|
|
54
67
|
retry_statuses: RETRY_STATUSES,
|
|
55
68
|
methods: %i[get post put patch delete]
|
|
56
|
-
|
|
69
|
+
# max_nesting caps deserialization depth so a hostile / malformed DSP
|
|
70
|
+
# response can't allocate unbounded memory (DoS guard on the parser).
|
|
71
|
+
f.response :json, content_type: /\bjson\z/, parser_options: { max_nesting: 50 }
|
|
57
72
|
f.response :logger, @configuration.logger, headers: false, bodies: false, log_level: :debug
|
|
58
73
|
f.adapter Faraday.default_adapter
|
|
59
74
|
f.options.timeout = @configuration.timeout
|
|
@@ -110,8 +125,7 @@ module DigiwinDsp
|
|
|
110
125
|
code: hash["error_code"] || hash["code"],
|
|
111
126
|
dsp_message: hash["error_message"] || hash["message"],
|
|
112
127
|
request_id: hash["request_id"],
|
|
113
|
-
http_status: status
|
|
114
|
-
response_body: body
|
|
128
|
+
http_status: status
|
|
115
129
|
}
|
|
116
130
|
end
|
|
117
131
|
|
|
@@ -120,9 +134,21 @@ module DigiwinDsp
|
|
|
120
134
|
code: body["Status"],
|
|
121
135
|
dsp_message: body["Message"],
|
|
122
136
|
request_id: body["request_id"],
|
|
123
|
-
http_status: 200
|
|
124
|
-
response_body: body
|
|
137
|
+
http_status: 200
|
|
125
138
|
}
|
|
126
139
|
end
|
|
140
|
+
|
|
141
|
+
# Block CRLF/LF/CR in header names and values. RFC 7230 §3.2.4 forbids
|
|
142
|
+
# them and Faraday + Net::HTTP catch many forms, but not all — close
|
|
143
|
+
# the gap to prevent header-injection / request-smuggling.
|
|
144
|
+
def sanitize_request_headers!(idempotency_key, headers)
|
|
145
|
+
sanitize_header!("X-Idempotency-Key", idempotency_key) if idempotency_key
|
|
146
|
+
headers.each { |k, v| sanitize_header!(k, v) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def sanitize_header!(name, value)
|
|
150
|
+
raise ArgumentError, "invalid header value for #{name.inspect}: contains CRLF/LF/CR" if value.to_s.match?(/[\r\n]/)
|
|
151
|
+
raise ArgumentError, "invalid header name #{name.inspect}: contains CRLF/LF/CR" if name.to_s.match?(/[\r\n]/)
|
|
152
|
+
end
|
|
127
153
|
end
|
|
128
154
|
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.1
|
|
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: []
|