cetustek 0.10.0 → 0.11.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 +26 -0
- data/README.md +24 -0
- data/lib/cetustek/configuration.rb +15 -0
- data/lib/cetustek/queries.rb +4 -4
- data/lib/cetustek/soap.rb +19 -1
- data/lib/cetustek/version.rb +1 -1
- data/lib/cetustek/xml.rb +8 -5
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0420f85e7559d22ad6d71556fde48379ebc1eb6bbc468613ccdc70cd8a5395fe
|
|
4
|
+
data.tar.gz: 42f8df11ccc3c81fb6157212cca9c8a8ff7f99dc873fd8f85f2ce58b66e6821b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 24d02adabed95531ab98e07470dad9192781a0e8b2ad293fc73962e8dbeb4e06b7bcab193cc9d2697620820f0b6e2f8ff569105e32b6fde400440b7b0d86920d
|
|
7
|
+
data.tar.gz: 5deefced44974194851bd8c628fb35a21c1a3435e6cced143bab22a195f4b478d4db02701cd893caec4aba005ba4b70664830c523af954363b2e365aea4a79cb
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.11.1] - 2026-09-04
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `QueryAllowance.find` 查無折讓單時會 raise `ResultError: nodata`,而不是回 nil。
|
|
13
|
+
`nodata` 的判斷原本是 `Xml.parse_response` 的 `nil_values:` 參數,只有
|
|
14
|
+
`QueryInvoice` 有傳。改成 `Xml::NIL_VALUES` 常數,所有查詢共用同一個判斷。
|
|
15
|
+
|
|
16
|
+
## [0.11.0] - 2026-08-24
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `config.transport` — `:httpi` (預設,維持現有行為) 或 `:faraday`。設成其他值
|
|
21
|
+
在 `Cetustek.configure` 當下就 raise `ArgumentError`。
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- 沙盒端點 `invoice.cetustek.com.tw` 的 `certificate verify failed`:savon 預設
|
|
26
|
+
的 HTTPI 首選 adapter 是 httpclient,而 httpclient 只認 gem 內附的
|
|
27
|
+
`cacert.pem`,裡面沒有該端點憑證鏈的根 Certum Trusted Root CA。只要 bundle
|
|
28
|
+
裡有 httpclient,沙盒開票就會全數失敗。`config.transport = :faraday` 改走
|
|
29
|
+
Faraday 的 `net_http`,讀系統信任庫。production (`www.ei.com.tw`) 不受影響。
|
|
30
|
+
Faraday 路徑不能傳 `open_timeout`/`read_timeout`(savon 2.17 的
|
|
31
|
+
`FaradayMigrationHint::OPTIONS` 會 raise),改由 `client.faraday.options`
|
|
32
|
+
設定,兩條路徑的 300 秒逾時一致。
|
|
33
|
+
|
|
8
34
|
## [0.10.0] - 2026-08-07
|
|
9
35
|
|
|
10
36
|
延續 0.9.0 的 code review:統一驗證力度與查詢介面,讓四個查詢類別與
|
data/README.md
CHANGED
|
@@ -50,9 +50,33 @@ Cetustek.configure do |config|
|
|
|
50
50
|
# Only the order id, invoice number and result code are logged (never the
|
|
51
51
|
# response body); the request XML is logged at debug level on failure.
|
|
52
52
|
config.logger = Rails.logger
|
|
53
|
+
|
|
54
|
+
# Optional. :httpi (default, savon's own HTTP stack) or :faraday.
|
|
55
|
+
# See "SSL on the sandbox endpoint" below — if sandbox calls fail with
|
|
56
|
+
# `certificate verify failed`, set this to :faraday.
|
|
57
|
+
config.transport = :faraday
|
|
53
58
|
end
|
|
54
59
|
```
|
|
55
60
|
|
|
61
|
+
### SSL on the sandbox endpoint (`certificate verify failed`)
|
|
62
|
+
|
|
63
|
+
The sandbox endpoint `invoice.cetustek.com.tw` chains to **Certum Trusted Root
|
|
64
|
+
CA**. Savon's default HTTP stack is HTTPI, whose first-choice adapter is
|
|
65
|
+
`httpclient` — and `httpclient` ignores the system trust store, trusting only
|
|
66
|
+
the `cacert.pem` bundled inside the gem, which does not include that root. So
|
|
67
|
+
if anything in your bundle pulls in `httpclient` (plenty of gems do), every
|
|
68
|
+
sandbox call fails with:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
SSL_connect ... certificate verify failed (unable to get local issuer certificate)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Production (`www.ei.com.tw`, Sectigo/USERTrust) is unaffected.
|
|
75
|
+
|
|
76
|
+
Fix it with `config.transport = :faraday`, which goes through Faraday's
|
|
77
|
+
`net_http` adapter and reads the system trust store. The 300s open/read
|
|
78
|
+
timeouts are applied either way.
|
|
79
|
+
|
|
56
80
|
## Usage
|
|
57
81
|
|
|
58
82
|
### Issue an Invoice
|
|
@@ -2,11 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
module Cetustek
|
|
4
4
|
class Configuration
|
|
5
|
+
# HTTPI (savon 的預設) 第一順位是 httpclient,而 httpclient 只認 gem 內附的
|
|
6
|
+
# cacert.pem,裡面沒有 Certum Trusted Root CA — 也就是 sandbox 端點
|
|
7
|
+
# invoice.cetustek.com.tw 憑證鏈的根。只要環境裡有 httpclient,沙盒開票就會
|
|
8
|
+
# 全數 certificate verify failed。改用 :faraday 走 net_http,讀系統信任庫。
|
|
9
|
+
TRANSPORTS = %i[httpi faraday].freeze
|
|
10
|
+
|
|
5
11
|
# logger is opt-in: nil means this gem writes nothing anywhere.
|
|
6
12
|
attr_accessor :environment, :site_id, :username, :password, :logger
|
|
13
|
+
attr_reader :transport
|
|
7
14
|
|
|
8
15
|
def initialize
|
|
9
16
|
@environment = :sandbox
|
|
17
|
+
@transport = :httpi
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def transport=(value)
|
|
21
|
+
raise ArgumentError, "transport must be one of #{TRANSPORTS.join(', ')}, got #{value.inspect}" \
|
|
22
|
+
unless TRANSPORTS.include?(value)
|
|
23
|
+
|
|
24
|
+
@transport = value
|
|
10
25
|
end
|
|
11
26
|
|
|
12
27
|
def url
|
data/lib/cetustek/queries.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Cetustek
|
|
|
30
30
|
# §2.5 QueryInvoicebyOrderid shares this exact XML shape (Table 13), so
|
|
31
31
|
# QueryInvoiceByOrderId#find reuses this instead of duplicating it.
|
|
32
32
|
def self.parse(xml)
|
|
33
|
-
root = Xml.parse_response(xml
|
|
33
|
+
root = Xml.parse_response(xml)
|
|
34
34
|
return nil unless root
|
|
35
35
|
|
|
36
36
|
data = Xml.parse_fields(root, FIELDS)
|
|
@@ -66,7 +66,7 @@ module Cetustek
|
|
|
66
66
|
# Query* classes — it used to be returned as if it were the number.
|
|
67
67
|
def self.find(order_id)
|
|
68
68
|
body = soap_return(query(order_id), :query_invoice_number_by_orderid).to_s.strip
|
|
69
|
-
return nil if body.empty? || body
|
|
69
|
+
return nil if body.empty? || Xml::NIL_VALUES.include?(body)
|
|
70
70
|
return body if body.match?(FORMAT)
|
|
71
71
|
|
|
72
72
|
ResultCode.raise!(body, ResultCode::COMMON)
|
|
@@ -90,8 +90,8 @@ module Cetustek
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# Same query, with the returned XML parsed into a Hash of snake_case keys
|
|
93
|
-
# plus a :details array. Values are the raw strings from the XML;
|
|
94
|
-
#
|
|
93
|
+
# plus a :details array. Values are the raw strings from the XML; nil when
|
|
94
|
+
# the platform answers with nothing at all, or the documented "nodata".
|
|
95
95
|
def self.find(allowance_number)
|
|
96
96
|
parse(soap_return(query(allowance_number), :query_allowance))
|
|
97
97
|
end
|
data/lib/cetustek/soap.rb
CHANGED
|
@@ -6,8 +6,26 @@ module Cetustek
|
|
|
6
6
|
# Shared SOAP plumbing: every operation hits the same WSDL and authenticates
|
|
7
7
|
# with 網站代碼+APIPassword (source) and the 租賃者統編 (rentid).
|
|
8
8
|
module Soap
|
|
9
|
+
TIMEOUT = 300
|
|
10
|
+
|
|
9
11
|
def soap_client
|
|
10
|
-
|
|
12
|
+
return httpi_client unless Cetustek.config.transport == :faraday
|
|
13
|
+
|
|
14
|
+
faraday_client
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def httpi_client
|
|
18
|
+
Savon.client(wsdl: Cetustek.config.url, open_timeout: TIMEOUT, read_timeout: TIMEOUT)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# savon 2.17 的 FaradayMigrationHint::OPTIONS 會讓 open_timeout/read_timeout
|
|
22
|
+
# 在 transport: :faraday 下直接 raise,逾時改由 faraday 連線自己設。
|
|
23
|
+
def faraday_client
|
|
24
|
+
require 'faraday'
|
|
25
|
+
Savon.client(wsdl: Cetustek.config.url, transport: :faraday).tap do |client|
|
|
26
|
+
client.faraday.options.open_timeout = TIMEOUT
|
|
27
|
+
client.faraday.options.read_timeout = TIMEOUT
|
|
28
|
+
end
|
|
11
29
|
end
|
|
12
30
|
|
|
13
31
|
def source
|
data/lib/cetustek/version.rb
CHANGED
data/lib/cetustek/xml.rb
CHANGED
|
@@ -8,6 +8,8 @@ module Cetustek
|
|
|
8
8
|
# and a single <Invoice>/<Allowance> root carrying XSDVersion.
|
|
9
9
|
module Xml
|
|
10
10
|
XSD_VERSION = '2.8'
|
|
11
|
+
# 查無資料時平台回的字串,不是結果代碼。
|
|
12
|
+
NIL_VALUES = %w[nodata].freeze
|
|
11
13
|
|
|
12
14
|
module_function
|
|
13
15
|
|
|
@@ -41,12 +43,13 @@ module Cetustek
|
|
|
41
43
|
end
|
|
42
44
|
|
|
43
45
|
# Shared guard every Query* class needs before it can parse a response
|
|
44
|
-
# body: nil for an empty answer or
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
46
|
+
# body: nil for an empty answer or the documented "nodata" sentinel;
|
|
47
|
+
# ResultError for a bare result code; otherwise the parsed root element,
|
|
48
|
+
# ready for parse_fields. "nodata" is not per-query — every 查詢 answers
|
|
49
|
+
# with it when the number simply isn't there, so it lives here.
|
|
50
|
+
def parse_response(body)
|
|
48
51
|
text = body.to_s.strip
|
|
49
|
-
return nil if text.empty? ||
|
|
52
|
+
return nil if text.empty? || NIL_VALUES.include?(text)
|
|
50
53
|
|
|
51
54
|
ResultCode.raise!(text, ResultCode::COMMON) unless text.start_with?('<')
|
|
52
55
|
|