digiwin_dsp 0.3.0 → 0.4.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 +49 -1
- data/README.md +44 -3
- data/lib/digiwin_dsp/client.rb +22 -2
- data/lib/digiwin_dsp/configuration.rb +1 -2
- data/lib/digiwin_dsp/enums.rb +131 -0
- data/lib/digiwin_dsp/resources/webhook_subscription.rb +8 -5
- data/lib/digiwin_dsp/version.rb +1 -1
- data/lib/digiwin_dsp/webhooks/event.rb +2 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b74fbc1739d80f99a563f002be9ab06212eb0ffce531ddf9144c9c8ceb28b34
|
|
4
|
+
data.tar.gz: 363e1faecee60d9a4c8f5cfe03b00bf2febd35f015f0034451e1c9c9e191308a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b14060dbe2c6d65890ffb925c1ee9f499a7870db0c4eb301646eaf0ea2907869e0e331d8ff14b76e1a6bd474a2de5bf75f97d9bdf5073d856a5c55b6753dd225
|
|
7
|
+
data.tar.gz: 1f1f7ab22cd0e077e455ce7cc6451442e5285ccf7e1c94a1abcb0c6e284e3fc86f94cf879170b4ddd2cc3d747ca532ba64a6082bf89be972aacadef23df33e7c
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,52 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-06-12
|
|
10
|
+
|
|
11
|
+
Ergonomics + cleanup release. One breaking change (dead config removal).
|
|
12
|
+
|
|
13
|
+
### BREAKING
|
|
14
|
+
|
|
15
|
+
- **`Configuration#api_secret` removed** (accessor, `DIGIWIN_DSP_API_SECRET` ENV read, README row, `.env.local.example` line). Reserved since v0.1.0 for "future HMAC signing" — DSPOOFFICIAL100 confirmed DSP doesn't sign webhooks, no gem code ever read it, and advertising it implied a security mechanism that didn't exist. **Migration:** delete `c.api_secret = ...` from your configure block (it would now raise `NoMethodError` at boot); a lingering ENV var is harmless.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`DigiwinDsp::Enums`** — DSP code tables as Ruby constants so callers stop hardcoding `"9104"`:
|
|
20
|
+
`OrderStatus`, `PayType` (15), `ShippingType` (9), `InvoiceStatus` (5), `InvoiceType` (10), `CarrierType` (5), `IsPay`, plus inbound-webhook `UpdateMode` and `DistributorCode`. Each has a frozen `ALL` array for caller-side validations. No forced client-side validation — DSP still rejects unknown codes via `WrongStatus:` → `ValidationError`.
|
|
21
|
+
- **Unknown-envelope warning.** If a 2xx Hash body matches neither known envelope shape (`Status/Message` nor `std_data.execution`), `Client` now `logger.warn`s before passing it through — so if DSP ships a third envelope, failures can't silently read as success.
|
|
22
|
+
- **Multi-line `last_record` live-verified.** A 2-line order with `"N"` on the non-final line was accepted by UAT (2026-06-12). The YAML spec's "blank means not-last" alternative conflicts with the gem's required-field check; the documented `"N"` convention is now the verified path.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `WebhookSubscription::ACTIONS` now derives from `Webhooks::ACTION_REGISTRY.keys` — what you can subscribe to is exactly what the gem can parse (single source of truth; previously two parallel lists that could drift).
|
|
27
|
+
|
|
28
|
+
### Docs
|
|
29
|
+
|
|
30
|
+
- README gains a **Troubleshooting / FAQ** section (HTTP-200-failure gotcha, `序號驗證失敗`, `SalesNotCreate` timing, `Shipped:` permanence, `WrongStatus` self-correction, dedupe-on-`form_no` surprises, webhook-delivery checklist, ERP invoice-visibility caveat).
|
|
31
|
+
- README documents the **built-in Faraday retry** (up to 4 attempts, exponential backoff + jitter) and how it stacks with ActiveJob/Sidekiq retries — worst-case latency math included.
|
|
32
|
+
- README flags DSPOOFFICIAL004's 個案 caveat: invoice data is only visible inside the ERP after per-customer Digiwin customization.
|
|
33
|
+
|
|
34
|
+
## [0.3.1] - 2026-06-12
|
|
35
|
+
|
|
36
|
+
Hardening patch from a full gem + docs review. All fixes grounded in the vendor YAML specs.
|
|
37
|
+
|
|
38
|
+
### Fixed
|
|
39
|
+
|
|
40
|
+
- **TLS failures now raise `DigiwinDsp::NetworkError`.** `Faraday::SSLError` sits directly under `Faraday::Error` (not `ConnectionFailed`), so certificate problems previously leaked as raw Faraday exceptions that `rescue DigiwinDsp::Error` could not catch.
|
|
41
|
+
- **Three more DSP failure messages classify into typed exceptions** instead of falling through to generic `Error`:
|
|
42
|
+
- `Shipped:訂單已出貨,不可取消` (DSPOOFFICIAL002) → `ValidationError` — permanent; the order left the warehouse
|
|
43
|
+
- `Processing:新增訂單處理中,不可取消` (DSPOOFFICIAL002) → `RateLimitError` — retryable once ERP processes the add
|
|
44
|
+
- `SalesNotCreate:銷貨單未成立` (DSPOOFFICIAL004) → `RateLimitError` — retryable once ERP converts the sales doc
|
|
45
|
+
Sidekiq/ActiveJob retry strategies can now distinguish "don't retry" from "retry later" on cancel and invoice flows.
|
|
46
|
+
- **`Webhooks::Event.parse_json` / `.extract_request` are now private class methods.** They were internal helpers accidentally exposed on all three event classes.
|
|
47
|
+
- **`WebhookSubscription` rejects non-`https://` callback addresses** at registration time. DSPOOFFICIAL100 mandates HTTPS callbacks, and with no HMAC signing a plaintext callback would be indefensible. (Previously the README claimed this was enforced when it wasn't.)
|
|
48
|
+
|
|
49
|
+
### Docs
|
|
50
|
+
|
|
51
|
+
- `dsp-api-spec.md`: corrected the e-invoice carrier field name — `Resources::Invoice` sends **`carrier_type`** (DSPOOFFICIAL004:164), not `carrier_code`. `carrier_code` is only on `Resources::Order` and the inbound webhook. Optional fields aren't validated client-side, so the wrong name silently drops carrier data.
|
|
52
|
+
- `dsp-api-spec.md`: failure-message table now covers 002 (cancel) and 004 (invoice) sources, not just 001.
|
|
53
|
+
- README: webhook security bullet now describes the actual https enforcement.
|
|
54
|
+
|
|
9
55
|
## [0.3.0] - 2026-05-28
|
|
10
56
|
|
|
11
57
|
DSP webhook support (DSPOOFFICIAL100). Lets a Rails app register a callback URL with DSP, then receive and parse the three documented ERP-originated push events: inventory updates, shipping/logistics status, and invoice issuance.
|
|
@@ -205,7 +251,9 @@ Initial release. Covers the four Self-hosted Website Module (自有官網模組)
|
|
|
205
251
|
- The gem is **synchronous on purpose**. Callers wrap requests in their own background job runner (e.g. ActiveJob) when needed.
|
|
206
252
|
- Idempotency: clients can send `X-Idempotency-Key` via the `idempotency_key:` kwarg. DSP also dedupes server-side by `form_no + platform_id`.
|
|
207
253
|
|
|
208
|
-
[Unreleased]: https://github.com/7a6163/digiwin_dsp/compare/v0.
|
|
254
|
+
[Unreleased]: https://github.com/7a6163/digiwin_dsp/compare/v0.4.0...HEAD
|
|
255
|
+
[0.4.0]: https://github.com/7a6163/digiwin_dsp/compare/v0.3.1...v0.4.0
|
|
256
|
+
[0.3.1]: https://github.com/7a6163/digiwin_dsp/compare/v0.3.0...v0.3.1
|
|
209
257
|
[0.3.0]: https://github.com/7a6163/digiwin_dsp/compare/v0.2.4...v0.3.0
|
|
210
258
|
[0.2.4]: https://github.com/7a6163/digiwin_dsp/compare/v0.2.3...v0.2.4
|
|
211
259
|
[0.2.3]: https://github.com/7a6163/digiwin_dsp/compare/v0.2.2...v0.2.3
|
data/README.md
CHANGED
|
@@ -51,7 +51,6 @@ Every setting also falls back to an ENV var:
|
|
|
51
51
|
| Setting | ENV var | Default | Notes |
|
|
52
52
|
|---|---|---|---|
|
|
53
53
|
| `api_key` | `DIGIWIN_DSP_API_KEY` | _(required)_ | sent as `DSP-api-key` header |
|
|
54
|
-
| `api_secret` | `DIGIWIN_DSP_API_SECRET` | `nil` | reserved for future HMAC signing; unused today |
|
|
55
54
|
| `platform_id` | `DIGIWIN_DSP_PLATFORM_ID` | `nil` | sent **per-record** in `request_detail.platform_id` (not in auth headers) |
|
|
56
55
|
| `environment` | `DIGIWIN_DSP_ENV` | `:sandbox` | `:sandbox` (UAT) or `:production` |
|
|
57
56
|
| `base_url` | `DIGIWIN_DSP_BASE_URL` | resolved from `environment` | must be `https://` and have a host in `allowed_hosts` |
|
|
@@ -119,7 +118,7 @@ response_detail = DigiwinDsp::Resources::Order.create(record)
|
|
|
119
118
|
# => [{ "form_no" => "WEB202605200001", ... }]
|
|
120
119
|
```
|
|
121
120
|
|
|
122
|
-
Multi-line orders: pass an array. Each element must carry the order-level fields plus its own line fields. Set `"last_record" => "Y"` on the final element and `"N"` on the rest:
|
|
121
|
+
Multi-line orders: pass an array. Each element must carry the order-level fields plus its own line fields. Set `"last_record" => "Y"` on the final element and `"N"` on the rest (live-verified against UAT 2026-06-12; the YAML spec says blank means "not last", but the gem's required-field check rejects blanks and DSP accepts the explicit `"N"`):
|
|
123
122
|
|
|
124
123
|
```ruby
|
|
125
124
|
records = [
|
|
@@ -144,6 +143,8 @@ Each has its own required-field set (8 / 11 / 19 fields respectively). Inspect `
|
|
|
144
143
|
DigiwinDsp::Serializers::CancellationSerializer::REQUIRED_FIELDS
|
|
145
144
|
```
|
|
146
145
|
|
|
146
|
+
> ⚠️ **Invoice sync requires ERP-side customization.** Per DSPOOFFICIAL004's spec note (個案), DSP accepts your invoice data unconditionally, but it only becomes *visible inside the ERP* after Digiwin performs per-customer integration work. If invoices appear to sync successfully but the ERP team can't see them, this is why — confirm the customization with your Digiwin contact before debugging your own code.
|
|
147
|
+
|
|
147
148
|
### `order_status` enum
|
|
148
149
|
|
|
149
150
|
Each endpoint requires a specific `order_status` value inside `request_detail`. DSP rejects others with `WrongStatus:order_status錯誤,請固定給N(...)`. The OpenAPI examples don't document this — verified live against UAT 2026-05-21:
|
|
@@ -213,12 +214,26 @@ end
|
|
|
213
214
|
```
|
|
214
215
|
|
|
215
216
|
> ⚠️ **DSP does NOT sign inbound webhooks.** There is no HMAC header. Defend the callback endpoint with:
|
|
216
|
-
> - HTTPS-only
|
|
217
|
+
> - HTTPS-only — `WebhookSubscription` rejects non-`https://` addresses at registration time (DSP's own spec mandates HTTPS callbacks)
|
|
217
218
|
> - An unguessable URL path (treat it as a secret)
|
|
218
219
|
> - An IP allowlist for DSP's egress range if your network team can get one
|
|
219
220
|
> - Replying `200 OK` within 30 seconds (DSP will retry and may eventually block your endpoint if too many calls fail)
|
|
220
221
|
> - **Idempotency by `form_no` / `invoice_number` / etc. on your side** — DSP may retry the same event
|
|
221
222
|
|
|
223
|
+
### Built-in retry (read before stacking job retries)
|
|
224
|
+
|
|
225
|
+
Every `Client#post` already retries transparently inside Faraday:
|
|
226
|
+
|
|
227
|
+
| Setting | Value |
|
|
228
|
+
|---|---|
|
|
229
|
+
| Attempts | up to 4 (1 original + max 3 retries) |
|
|
230
|
+
| Triggers | HTTP 429, 500, 502, 503, 504, connection failures |
|
|
231
|
+
| Backoff | exponential — ~0.5s, ~1s, ~2s between attempts, ±50% jitter |
|
|
232
|
+
|
|
233
|
+
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.
|
|
234
|
+
|
|
235
|
+
The built-in retry covers transport-level blips; envelope-level "retry later" signals (`Processing:資料處理中`, `SalesNotCreate:` etc. → `RateLimitError`) are deliberately left to your job layer, where you control scheduling.
|
|
236
|
+
|
|
222
237
|
### Background jobs
|
|
223
238
|
|
|
224
239
|
The gem is synchronous on purpose. Wrap calls in your own job runner:
|
|
@@ -265,6 +280,32 @@ rescue DigiwinDsp::RateLimitError, DigiwinDsp::ServerError
|
|
|
265
280
|
end
|
|
266
281
|
```
|
|
267
282
|
|
|
283
|
+
## Troubleshooting / FAQ
|
|
284
|
+
|
|
285
|
+
**"My request succeeded with HTTP 200 but raised an exception?"**
|
|
286
|
+
That's DSP's design — application failures come back as HTTP 200 with `Status:"Failure"` in the body. The gem parses the envelope and raises the matching typed exception. Trust the exception, not the HTTP status.
|
|
287
|
+
|
|
288
|
+
**`AuthenticationError: DSP 序號驗證失敗`**
|
|
289
|
+
Your `DSP-api-key` is wrong, expired, or for the other environment (UAT keys don't work on production and vice versa). Note this arrives as HTTP 200, not 401.
|
|
290
|
+
|
|
291
|
+
**`RateLimitError: ...SalesNotCreate:銷貨單未成立`** (invoice sync)
|
|
292
|
+
The ERP hasn't converted the order into a sales document yet — this is a timing issue, not a bug. Retry later (the exception type is retryable by design). If it persists for hours, ask your Digiwin contact whether order conversion is running.
|
|
293
|
+
|
|
294
|
+
**`ValidationError: ...Shipped:訂單已出貨,不可取消`** (cancel)
|
|
295
|
+
Permanent — the order left the warehouse. Don't retry; surface to your support flow instead.
|
|
296
|
+
|
|
297
|
+
**`ValidationError: ...WrongStatus:order_status錯誤,請固定給N(...)`**
|
|
298
|
+
You sent the wrong `order_status` for that endpoint. DSP's message tells you the expected value; or just use `DigiwinDsp::Enums::OrderStatus` constants.
|
|
299
|
+
|
|
300
|
+
**`DuplicateRequestError` on a brand-new order**
|
|
301
|
+
DSP dedupes on `form_no + platform_id` forever — including orders created in earlier tests. Generate unique `form_no` values per environment.
|
|
302
|
+
|
|
303
|
+
**Inventory webhook never arrives**
|
|
304
|
+
Webhook delivery requires (1) a successful `WebhookSubscription.create` for that exact `action`, (2) an HTTPS endpoint answering `200` within 30s, and (3) the ERP actually emitting the event. Check all three, in that order.
|
|
305
|
+
|
|
306
|
+
**Invoices sync but the ERP team can't see them**
|
|
307
|
+
See the invoice caveat above — DSPOOFFICIAL004 requires per-customer ERP customization (個案) before invoice data is visible in the ERP.
|
|
308
|
+
|
|
268
309
|
## Custom `digi_header`
|
|
269
310
|
|
|
270
311
|
By default the gem **omits `digi_header`** from the request body (it's only required for certain custom Digiwin integrations). If your DSP setup expects one, pass it through:
|
data/lib/digiwin_dsp/client.rb
CHANGED
|
@@ -33,6 +33,9 @@ module DigiwinDsp
|
|
|
33
33
|
[/Duplicated:/, DuplicateRequestError], # order already exists
|
|
34
34
|
[/Processing:資料處理中/, RateLimitError], # transient; retry later
|
|
35
35
|
[/Processing:取消訂單處理中/, ValidationError], # cancel in flight
|
|
36
|
+
[/Processing:新增訂單處理中/, RateLimitError], # add in flight; cancel retryable after ERP processes (002)
|
|
37
|
+
[/Shipped:/, ValidationError], # already shipped — cancel permanently impossible (002)
|
|
38
|
+
[/SalesNotCreate:/, RateLimitError], # ERP hasn't converted sales doc yet; invoice retryable (004)
|
|
36
39
|
[/WrongStatus:/, ValidationError], # bad payload
|
|
37
40
|
[/系統異常:/, ServerError] # DSP internal error
|
|
38
41
|
].freeze
|
|
@@ -52,7 +55,10 @@ module DigiwinDsp
|
|
|
52
55
|
req.body = body
|
|
53
56
|
end
|
|
54
57
|
handle_response(response)
|
|
55
|
-
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
58
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError, Faraday::SSLError => e
|
|
59
|
+
# SSLError sits directly under Faraday::Error (not ConnectionFailed),
|
|
60
|
+
# so it needs an explicit entry — otherwise TLS failures leak as raw
|
|
61
|
+
# Faraday exceptions that `rescue DigiwinDsp::Error` won't catch.
|
|
56
62
|
raise NetworkError, e.message
|
|
57
63
|
end
|
|
58
64
|
|
|
@@ -126,11 +132,25 @@ module DigiwinDsp
|
|
|
126
132
|
return nil if exec["code"].to_s == "0"
|
|
127
133
|
|
|
128
134
|
{ message: exec["description"].to_s, code: exec["code"] }
|
|
129
|
-
elsif body
|
|
135
|
+
elsif body.key?("Status")
|
|
136
|
+
return nil unless body["Status"].to_s.casecmp("failure").zero?
|
|
137
|
+
|
|
130
138
|
{ message: body["Message"].to_s, code: body["Status"] }
|
|
139
|
+
else
|
|
140
|
+
warn_unknown_envelope(body)
|
|
141
|
+
nil
|
|
131
142
|
end
|
|
132
143
|
end
|
|
133
144
|
|
|
145
|
+
# Neither known envelope. If DSP ships a third shape, failures would
|
|
146
|
+
# otherwise pass through as "success" silently — leave a trace.
|
|
147
|
+
def warn_unknown_envelope(body)
|
|
148
|
+
@configuration.logger.warn(
|
|
149
|
+
"digiwin_dsp: response matched no known envelope shape " \
|
|
150
|
+
"(keys: #{body.keys.inspect}); passing body through unchanged"
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
134
154
|
def classify_envelope_failure(message, code:, body:)
|
|
135
155
|
klass = ENVELOPE_FAILURE_MAP.find { |regex, _| regex.match?(message) }&.last || Error
|
|
136
156
|
klass.new(message, code: code, dsp_message: message, request_id: body["request_id"], http_status: 200)
|
|
@@ -21,13 +21,12 @@ module DigiwinDsp
|
|
|
21
21
|
production: "https://digiwindsp.digiwin.com/DSP/api/webhook"
|
|
22
22
|
}.freeze
|
|
23
23
|
|
|
24
|
-
attr_accessor :api_key, :
|
|
24
|
+
attr_accessor :api_key, :platform_id, :environment, :logger,
|
|
25
25
|
:timeout, :open_timeout, :allowed_hosts
|
|
26
26
|
attr_writer :base_url, :webhook_base_url
|
|
27
27
|
|
|
28
28
|
def initialize
|
|
29
29
|
@api_key = ENV["DIGIWIN_DSP_API_KEY"]
|
|
30
|
-
@api_secret = ENV["DIGIWIN_DSP_API_SECRET"]
|
|
31
30
|
@platform_id = ENV["DIGIWIN_DSP_PLATFORM_ID"]
|
|
32
31
|
@environment = ENV.fetch("DIGIWIN_DSP_ENV") { "sandbox" }.to_sym
|
|
33
32
|
@base_url = ENV["DIGIWIN_DSP_BASE_URL"]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DigiwinDsp
|
|
4
|
+
# DSP wire-format code tables as Ruby constants, so callers write
|
|
5
|
+
# `Enums::PayType::CREDIT_CARD` instead of a bare "9104".
|
|
6
|
+
#
|
|
7
|
+
# Source of truth: docs/dsp-specs/*.yaml (see docs/dsp-api-spec.md for the
|
|
8
|
+
# readable digest). The gem deliberately does NOT validate these values
|
|
9
|
+
# client-side — DSP rejects unknown codes with `WrongStatus:` →
|
|
10
|
+
# `ValidationError`. The `ALL` arrays exist for caller-side checks
|
|
11
|
+
# (e.g. dropdowns, model validations) if you want them.
|
|
12
|
+
module Enums
|
|
13
|
+
# request_detail.order_status — fixed per endpoint (live-verified).
|
|
14
|
+
module OrderStatus
|
|
15
|
+
CANCEL = "2" # Resources::Cancellation
|
|
16
|
+
NEW_ORDER = "3" # Resources::Order
|
|
17
|
+
INVOICE = "5" # Resources::Invoice
|
|
18
|
+
RETURN = "7" # Resources::Return
|
|
19
|
+
|
|
20
|
+
ALL = [CANCEL, NEW_ORDER, INVOICE, RETURN].freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# request_detail.pay_type on Resources::Order (DSPOOFFICIAL001:163-179).
|
|
24
|
+
module PayType
|
|
25
|
+
OTHER = "9100" # 其他收款方式
|
|
26
|
+
JKO_PAY = "9101" # 街口支付
|
|
27
|
+
CVS_COD = "9102" # 超商取貨付款
|
|
28
|
+
GOOGLE_PAY = "9103"
|
|
29
|
+
CREDIT_CARD = "9104" # 信用卡一次付款
|
|
30
|
+
CASH_ON_DELIVERY = "9105" # 貨到付款
|
|
31
|
+
AFTEE = "9106" # AFTEE 先享後付
|
|
32
|
+
APPLE_PAY = "9107"
|
|
33
|
+
ATM = "9108" # ATM 付款
|
|
34
|
+
CREDIT_CARD_INSTALLMENT = "9109" # 信用卡分期付款
|
|
35
|
+
EASY_WALLET = "9110" # 悠遊付
|
|
36
|
+
LINE_PAY = "9111"
|
|
37
|
+
PAYPAL_EXPRESS = "9112"
|
|
38
|
+
FREE_CHECKOUT = "9113" # 免費結帳
|
|
39
|
+
CVS_PAYMENT_CODE = "9114" # 超商代碼繳費
|
|
40
|
+
|
|
41
|
+
ALL = [OTHER, JKO_PAY, CVS_COD, GOOGLE_PAY, CREDIT_CARD, CASH_ON_DELIVERY,
|
|
42
|
+
AFTEE, APPLE_PAY, ATM, CREDIT_CARD_INSTALLMENT, EASY_WALLET,
|
|
43
|
+
LINE_PAY, PAYPAL_EXPRESS, FREE_CHECKOUT, CVS_PAYMENT_CODE].freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# request_detail.shipping_type on Resources::Order (DSPOOFFICIAL001:186-200).
|
|
47
|
+
module ShippingType
|
|
48
|
+
OTHER = "9100" # 其他取貨方式
|
|
49
|
+
INTERNATIONAL = "9101" # 海外宅配
|
|
50
|
+
HOME_DELIVERY_COD = "9102" # 宅配貨到付款
|
|
51
|
+
STORE_PICKUP_PAID = "9103" # 付款後門市自取
|
|
52
|
+
HOME_DELIVERY_PAID = "9104" # 宅配(含離島)已付款只取貨
|
|
53
|
+
CVS_PICKUP_PAID = "9105" # 付款後超商取貨
|
|
54
|
+
CVS_PICKUP_COD = "9106" # 超商取貨付款
|
|
55
|
+
HOME_DELIVERY_CASH = "9107" # 宅配(含離島)貨到付現
|
|
56
|
+
HOME_DELIVERY_CARD = "9108" # 宅配(含離島)貨到刷卡
|
|
57
|
+
|
|
58
|
+
ALL = [OTHER, INTERNATIONAL, HOME_DELIVERY_COD, STORE_PICKUP_PAID,
|
|
59
|
+
HOME_DELIVERY_PAID, CVS_PICKUP_PAID, CVS_PICKUP_COD,
|
|
60
|
+
HOME_DELIVERY_CASH, HOME_DELIVERY_CARD].freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# request_detail.invoice_status on Resources::Invoice (DSPOOFFICIAL004:110-122).
|
|
64
|
+
module InvoiceStatus
|
|
65
|
+
ISSUED = "1" # 開立
|
|
66
|
+
VOIDED = "2" # 作廢
|
|
67
|
+
ALLOWANCE = "3" # 折讓
|
|
68
|
+
CANCELLED = "4" # 註銷
|
|
69
|
+
ALLOWANCE_VOIDED = "5" # 折讓作廢
|
|
70
|
+
|
|
71
|
+
ALL = [ISSUED, VOIDED, ALLOWANCE, CANCELLED, ALLOWANCE_VOIDED].freeze
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# request_detail.invoice_type on Resources::Invoice (DSPOOFFICIAL004:123-140).
|
|
75
|
+
module InvoiceType
|
|
76
|
+
DUPLICATE = "1" # 二聯式
|
|
77
|
+
TRIPLICATE = "2" # 三聯式
|
|
78
|
+
DUPLICATE_REGISTER = "3" # 二聯式收銀機發票
|
|
79
|
+
TRIPLICATE_REGISTER = "4" # 三聯式收銀機發票
|
|
80
|
+
COMPUTER = "5" # 電子計算機發票
|
|
81
|
+
EXEMPT = "6" # 免用統一發票
|
|
82
|
+
E_INVOICE = "7" # 電子發票 — modern default in Taiwan
|
|
83
|
+
CHINA_VAT_SPECIAL = "A" # 增值稅專用發票
|
|
84
|
+
CHINA_GENERAL = "B" # 普通發票
|
|
85
|
+
CHINA_EXEMPT = "C" # 免用發票
|
|
86
|
+
|
|
87
|
+
ALL = [DUPLICATE, TRIPLICATE, DUPLICATE_REGISTER, TRIPLICATE_REGISTER,
|
|
88
|
+
COMPUTER, EXEMPT, E_INVOICE, CHINA_VAT_SPECIAL, CHINA_GENERAL,
|
|
89
|
+
CHINA_EXEMPT].freeze
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# E-invoice carrier codes (DSPOOFFICIAL004:167-176). ⚠️ Field name varies:
|
|
93
|
+
# `carrier_type` on Resources::Invoice, `carrier_code` on Resources::Order
|
|
94
|
+
# and the inbound Webhooks::InvoiceUpdate payload.
|
|
95
|
+
module CarrierType
|
|
96
|
+
IPASS = "1H0001" # 一卡通
|
|
97
|
+
EASYCARD = "1K0001" # 悠遊卡
|
|
98
|
+
ICASH = "2G0001"
|
|
99
|
+
MOBILE_BARCODE = "3J0002" # 手機條碼 — most common
|
|
100
|
+
CITIZEN_CERT = "CQ0001" # 自然人憑證
|
|
101
|
+
|
|
102
|
+
ALL = [IPASS, EASYCARD, ICASH, MOBILE_BARCODE, CITIZEN_CERT].freeze
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# request_detail.is_pay on Resources::Invoice (DSPOOFFICIAL004:189-195).
|
|
106
|
+
module IsPay
|
|
107
|
+
UNPAID = "0"
|
|
108
|
+
PAID = "1"
|
|
109
|
+
|
|
110
|
+
ALL = [UNPAID, PAID].freeze
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# spec_list[].update_mode on inbound Webhooks::InventoryUpdate
|
|
114
|
+
# (DSPOOFFICIAL100). "adjust" requires per-customer ERP customization.
|
|
115
|
+
module UpdateMode
|
|
116
|
+
TOTAL = "total" # stock is the new total
|
|
117
|
+
ADJUST = "adjust" # stock is a delta (can be negative)
|
|
118
|
+
|
|
119
|
+
ALL = [TOTAL, ADJUST].freeze
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# distributor_code on inbound Webhooks::LogisticsUpdate (DSPOOFFICIAL100).
|
|
123
|
+
# Spec lists only these two; handle unknown carriers defensively.
|
|
124
|
+
module DistributorCode
|
|
125
|
+
HCT = "HCT" # 新竹倉儲
|
|
126
|
+
CAT = "CAT" # 統一速達(黑貓)
|
|
127
|
+
|
|
128
|
+
ALL = [HCT, CAT].freeze
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -12,11 +12,9 @@ module DigiwinDsp
|
|
|
12
12
|
class WebhookSubscription
|
|
13
13
|
PATH = "/v1/webhook"
|
|
14
14
|
DEFAULT_PROD = "OFFICIALWEBSITE"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
invoice/update
|
|
19
|
-
].freeze
|
|
15
|
+
# Single source of truth lives in Webhooks::ACTION_REGISTRY — what you
|
|
16
|
+
# can subscribe to is exactly what the gem can parse.
|
|
17
|
+
ACTIONS = Webhooks::ACTION_REGISTRY.keys.freeze
|
|
20
18
|
ADDRESS_MAX_LENGTH = 500
|
|
21
19
|
|
|
22
20
|
def self.create(action:, address:, platform_id: nil, prod: DEFAULT_PROD)
|
|
@@ -43,6 +41,11 @@ module DigiwinDsp
|
|
|
43
41
|
"action must be one of #{ACTIONS.inspect} (got #{action.inspect})"
|
|
44
42
|
end
|
|
45
43
|
raise DigiwinDsp::ValidationError, "address is required" if address.nil? || address.to_s.empty?
|
|
44
|
+
|
|
45
|
+
# DSPOOFFICIAL100 mandates HTTPS for the callback ("必須在 30 秒內以
|
|
46
|
+
# HTTPS 回應") — and with no HMAC signing, a plaintext callback URL
|
|
47
|
+
# would be indefensible anyway.
|
|
48
|
+
raise DigiwinDsp::ValidationError, "address must be an https:// URL (got #{address.inspect})" unless address.start_with?("https://")
|
|
46
49
|
return unless address.length > ADDRESS_MAX_LENGTH
|
|
47
50
|
|
|
48
51
|
raise DigiwinDsp::ValidationError,
|
data/lib/digiwin_dsp/version.rb
CHANGED
|
@@ -30,6 +30,8 @@ module DigiwinDsp
|
|
|
30
30
|
raise(ParseError, "envelope missing digi_body.std_data.parameter.request")
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
private_class_method :parse_json, :extract_request
|
|
34
|
+
|
|
33
35
|
def initialize(digi_header:, request:, raw:)
|
|
34
36
|
@digi_header = digi_header
|
|
35
37
|
@request = request
|
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.4.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-06-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -68,6 +68,7 @@ files:
|
|
|
68
68
|
- lib/digiwin_dsp/authenticator.rb
|
|
69
69
|
- lib/digiwin_dsp/client.rb
|
|
70
70
|
- lib/digiwin_dsp/configuration.rb
|
|
71
|
+
- lib/digiwin_dsp/enums.rb
|
|
71
72
|
- lib/digiwin_dsp/resources/base.rb
|
|
72
73
|
- lib/digiwin_dsp/resources/cancellation.rb
|
|
73
74
|
- lib/digiwin_dsp/resources/invoice.rb
|