cetustek 0.11.0 → 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6a6aca36dc760878fe11e9107416c7296ef5fa7835d1e223f3cab8248970fb7
4
- data.tar.gz: 96fb067d26a7cb40e0b429a8a9f502fa486bc5bc2efcded04bb136bcba4b99c1
3
+ metadata.gz: f87e7bbfa74ad309c50b741139f8cfd66f7a197813c2e2b8d515d4e810bd0ef2
4
+ data.tar.gz: 89e531c88d9aff213eae2e6c75a089a7121abd5a8604d0abeadbed3bc255a0c5
5
5
  SHA512:
6
- metadata.gz: 4cdf8f6a063bb979aecd5d974dd1482329582359831bf0cbb20f46c50987dbbb677e675dd3507c3bbd69883277103760900dc12b55c3650f3215b1e3ab3dc408
7
- data.tar.gz: 6240dd9f19af70f835d4323a5fad194a77ef8be409f503013887095c1bd25ece1331507030443ae8a7ea571aa97c6a47eb6982e0142a2605f467afbd47977564
6
+ metadata.gz: 0af507e99327c3b63d4aee2f04398722cb7cf167604027fdacbaccdf374aa2e0300c21bf904dbd366f2390c129df0a34d36a9d46f9fb7e1471a941f11b1b3f0b
7
+ data.tar.gz: 2be9f5c9c30991581dc725b53235abdd608ea5b1e3c6ac356ec7f2f52dc7dde0d0874bdfcc630f508548a8c578f9806372e3ae80703cc2141203f1f6a6443148
data/CHANGELOG.md CHANGED
@@ -5,6 +5,40 @@ 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.12.0] - 2026-09-09
9
+
10
+ ### Fixed
11
+
12
+ - 亂填的代碼欄位不再被當成 0 靜靜送出去。`'x'.to_i` 是 `0`,而 `0` 是
13
+ `donate_mark` 的「載具」與 `round_num` 的「四捨五入」有效代碼,所以
14
+ `donate_mark: 'x'` 原本不會被擋,會被當成載具發票開出去。`tax_type: '1abc'`
15
+ 會變成應稅,`CreateAllowance` 的 `check_allowance: 'x'` 也能通過「必須為 0」
16
+ 的檢查。發票與折讓兩條路徑改用共用的字串比對。
17
+
18
+ ### Changed
19
+
20
+ - **Breaking**: 最低 Ruby 版本提高到 3.3.0。3.0 已於 2024-04 EOL,不再收安全更新。
21
+ - **Breaking**: 上述代碼欄位原本會被默默接受的值現在會 raise `ArgumentError`。
22
+ 影響的是本來就送錯值的呼叫端 —— 那些請求送到平台也是失敗,只是現在提早失敗。
23
+ - `Cetustek::Xml` 從 `module_function` 改為 `extend self`。`Xml.foo` 與
24
+ include 後的呼叫方式都不變。
25
+
26
+ ### Internal
27
+
28
+ - 導入 mutant 做 mutation testing(`bundle exec mutant run`)。mutation coverage
29
+ 從 57.42% 提升到 97.13%,行覆蓋率 100%。上面那個 bug 就是這樣挖出來的。
30
+ - 每週排程跑 mutation testing,掉出門檻才失敗(`.github/workflows/mutant.yml`)。
31
+ - 移除 `configuration.rb` 裡與 `lib/cetustek.rb` 重複的 `Cetustek.config` 定義
32
+ (被後者蓋掉的死碼)。
33
+
34
+ ## [0.11.1] - 2026-09-04
35
+
36
+ ### Fixed
37
+
38
+ - `QueryAllowance.find` 查無折讓單時會 raise `ResultError: nodata`,而不是回 nil。
39
+ `nodata` 的判斷原本是 `Xml.parse_response` 的 `nil_values:` 參數,只有
40
+ `QueryInvoice` 有傳。改成 `Xml::NIL_VALUES` 常數,所有查詢共用同一個判斷。
41
+
8
42
  ## [0.11.0] - 2026-08-24
9
43
 
10
44
  ### Added
data/README.md CHANGED
@@ -368,7 +368,7 @@ Cetustek::PhoneBarcode.valid?('/ABC123') # => true / false
368
368
 
369
369
  ## Requirements
370
370
 
371
- - Ruby >= 3.0.0
371
+ - Ruby >= 3.3.0
372
372
  - `ox` gem for XML processing
373
373
  - `savon` gem for SOAP services
374
374
 
data/config/mutant.yml ADDED
@@ -0,0 +1,13 @@
1
+ # 對 lib/ 全部 subject 跑 mutation testing:bundle exec mutant run
2
+ # 開源專案用 --usage opensource,不需要 license key。
3
+ usage: opensource
4
+ integration:
5
+ name: rspec
6
+ requires:
7
+ - cetustek
8
+ matcher:
9
+ subjects:
10
+ - Cetustek*
11
+ coverage_criteria:
12
+ timeout: false
13
+ process_abort: true
@@ -40,14 +40,4 @@ module Cetustek
40
40
  @environment == :sandbox
41
41
  end
42
42
  end
43
-
44
- class << self
45
- def configure
46
- yield(config)
47
- end
48
-
49
- def config
50
- @config ||= Configuration.new
51
- end
52
- end
53
43
  end
@@ -21,7 +21,8 @@ module Cetustek
21
21
  # check_allowance 只剩 0(已確認的折讓單):規格 §2.9 已刪除 1,
22
22
  # 114/01/01 起上傳的折讓單皆為已確認。
23
23
  def initialize(allowance_data, check_allowance: 0)
24
- raise ArgumentError, 'check_allowance must be 0 (114/01/01 起折讓單皆為已確認)' unless check_allowance.to_i.zero?
24
+ # to_i 會把 'x' 也變成 0,字串比對才擋得住亂填的值。
25
+ raise ArgumentError, 'check_allowance must be 0 (114/01/01 起折讓單皆為已確認)' unless check_allowance.to_s == '0'
25
26
 
26
27
  @data = allowance_data
27
28
  @check_allowance = 0
@@ -53,7 +53,7 @@ module Cetustek
53
53
  end
54
54
 
55
55
  def validate_tax_type!
56
- return if TAX_TYPES.include?(@tax_type.to_i)
56
+ return if code_in?(TAX_TYPES, @tax_type)
57
57
 
58
58
  raise ArgumentError, "tax_type must be 1 (應稅), 2 (零稅率) or 3 (免稅), got #{@tax_type.inspect}"
59
59
  end
@@ -65,14 +65,19 @@ module Cetustek
65
65
  end
66
66
 
67
67
  def validate_round_num!
68
- return if @round_num.nil? || ROUND_NUMS.include?(@round_num.to_i)
68
+ return if @round_num.nil? || code_in?(ROUND_NUMS, @round_num)
69
69
 
70
70
  raise ArgumentError, "round_num must be between #{ROUND_NUMS.first} and #{ROUND_NUMS.last}, " \
71
71
  "got #{@round_num.inspect}"
72
72
  end
73
73
 
74
+ # 見 InvoiceData#code_in?:to_i 會把亂填的代碼變成 0,而 0 是有效代碼。
75
+ def code_in?(codes, value)
76
+ codes.map(&:to_s).include?(value.to_s)
77
+ end
78
+
74
79
  def blank?(value)
75
- value.nil? || value.to_s.strip.empty?
80
+ value.to_s.strip.empty?
76
81
  end
77
82
  end
78
83
  end
@@ -160,7 +160,7 @@ module Cetustek
160
160
  def validate_donate_mark!
161
161
  raise ArgumentError, 'donate_mark is required (0 載具, 1 捐贈, 2 紙本)' if blank?(@donate_mark)
162
162
 
163
- unless DONATE_MARKS.include?(@donate_mark.to_i)
163
+ unless code_in?(DONATE_MARKS, @donate_mark)
164
164
  raise ArgumentError, "donate_mark must be 0 (載具), 1 (捐贈) or 2 (紙本), got #{@donate_mark.inspect}"
165
165
  end
166
166
 
@@ -197,7 +197,7 @@ module Cetustek
197
197
  end
198
198
 
199
199
  def validate_tax!
200
- unless TAX_TYPES.include?(@tax_type.to_i)
200
+ unless code_in?(TAX_TYPES, @tax_type)
201
201
  raise ArgumentError, "tax_type must be one of #{TAX_TYPES.join(', ')}, got #{@tax_type.inspect}"
202
202
  end
203
203
 
@@ -217,20 +217,26 @@ module Cetustek
217
217
  end
218
218
 
219
219
  def validate_remark!
220
- return if @remark.nil? || @remark.to_s.length <= MAX_REMARK_LENGTH
220
+ return if @remark.to_s.length <= MAX_REMARK_LENGTH
221
221
 
222
222
  raise ArgumentError, "remark must not exceed #{MAX_REMARK_LENGTH} characters"
223
223
  end
224
224
 
225
225
  def validate_round_num!
226
- return if @round_num.nil? || ROUND_NUMS.include?(@round_num.to_i)
226
+ return if @round_num.nil? || code_in?(ROUND_NUMS, @round_num)
227
227
 
228
228
  raise ArgumentError, "round_num must be between #{ROUND_NUMS.first} and #{ROUND_NUMS.last}, " \
229
229
  "got #{@round_num.inspect}"
230
230
  end
231
231
 
232
+ # 'x'.to_i 是 0,而 0 是「載具」「四捨五入」的有效代碼 —— 用 to_i 比對,
233
+ # 亂填的值會被當成 0 靜靜送出去。代碼一律轉字串比對。
234
+ def code_in?(codes, value)
235
+ codes.map(&:to_s).include?(value.to_s)
236
+ end
237
+
232
238
  def blank?(value)
233
- value.nil? || value.to_s.strip.empty?
239
+ value.to_s.strip.empty?
234
240
  end
235
241
  end
236
242
 
@@ -273,7 +279,7 @@ module Cetustek
273
279
  end
274
280
 
275
281
  def blank?(value)
276
- value.nil? || value.to_s.strip.empty?
282
+ value.to_s.strip.empty?
277
283
  end
278
284
  end
279
285
  end
@@ -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, nil_values: %w[nodata])
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 == 'nodata'
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; returns
94
- # nil when the platform answers with nothing at all.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cetustek
4
- VERSION = "0.11.0"
4
+ VERSION = "0.12.0"
5
5
  end
data/lib/cetustek/xml.rb CHANGED
@@ -8,8 +8,13 @@ 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
- module_function
14
+ # extend self(而不是 module_function):module_function 會把方法複製一份到
15
+ # singleton,mutation testing 改到的是 instance method 那份,正式碼呼叫的
16
+ # 卻是複本,整個模組的變異都殺不掉。
17
+ extend self
13
18
 
14
19
  def document(root_name)
15
20
  doc = Ox::Document.new
@@ -41,12 +46,13 @@ module Cetustek
41
46
  end
42
47
 
43
48
  # Shared guard every Query* class needs before it can parse a response
44
- # body: nil for an empty answer or one of the documented sentinel strings
45
- # (e.g. "nodata"); ResultError for a bare result code; otherwise the
46
- # parsed root element, ready for parse_fields.
47
- def parse_response(body, nil_values: [])
49
+ # body: nil for an empty answer or the documented "nodata" sentinel;
50
+ # ResultError for a bare result code; otherwise the parsed root element,
51
+ # ready for parse_fields. "nodata" is not per-query — every 查詢 answers
52
+ # with it when the number simply isn't there, so it lives here.
53
+ def parse_response(body)
48
54
  text = body.to_s.strip
49
- return nil if text.empty? || nil_values.include?(text)
55
+ return nil if text.empty? || NIL_VALUES.include?(text)
50
56
 
51
57
  ResultCode.raise!(text, ResultCode::COMMON) unless text.start_with?('<')
52
58
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cetustek
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac
@@ -78,6 +78,7 @@ files:
78
78
  - CHANGELOG.md
79
79
  - README.md
80
80
  - Rakefile
81
+ - config/mutant.yml
81
82
  - lib/cetustek.rb
82
83
  - lib/cetustek/cancel_allowance.rb
83
84
  - lib/cetustek/cancel_invoice.rb
@@ -110,7 +111,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
110
111
  requirements:
111
112
  - - ">="
112
113
  - !ruby/object:Gem::Version
113
- version: 3.0.0
114
+ version: 3.3.0
114
115
  required_rubygems_version: !ruby/object:Gem::Requirement
115
116
  requirements:
116
117
  - - ">="