cetustek 0.6.0 → 0.7.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 +20 -0
- data/README.md +24 -1
- data/lib/cetustek/cancel_allowance.rb +17 -2
- data/lib/cetustek/create_allowance.rb +32 -4
- data/lib/cetustek/errors.rb +31 -0
- data/lib/cetustek/models/allowance_data.rb +8 -0
- data/lib/cetustek/queries.rb +41 -0
- data/lib/cetustek/version.rb +1 -1
- data/lib/cetustek.rb +1 -2
- 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: edd228bea5d7cd37feceb6be6f790272b6bd66c8638b70518ea13d8a23454dcb
|
|
4
|
+
data.tar.gz: a365620162f118e193b9ddaa1caf50e335e43bac777831cfcff5c2b7322aff3e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7849c00fa5e89b28fb010dc4e71061b464a06aa9ea95d8213d135c5dcf031a80fe693fa79912c96bcef6d6f7c2e9a2dc0db9e34be516cc50ab6b62f433221b1f
|
|
7
|
+
data.tar.gz: 94041a6cc510de760b50799e7b8aa8d23159da26e93a1b0312c3f2be28decd51b925504ce416a9ac48ea8c04dc412e463437dcc69a7dcff020c50b9fbc54a482
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ 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.7.0] - 2026-08-04
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `Cetustek::QueryAllowance.find(allowance_number)` — 查詢折讓資料 (§2.11) with the
|
|
12
|
+
returned XML parsed into a Hash (snake_case keys + `:details` array); `nil` when the
|
|
13
|
+
allowance number is unknown. `.query` still returns the raw Savon response
|
|
14
|
+
- `Cetustek::ResultError` (with `#code`) plus the full 折讓 result-code tables from
|
|
15
|
+
spec Table 17/19, including the suffixed forms (`M:欄位`, `D2_3`)
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Breaking:** `CreateAllowance#execute` / `CancelAllowance#execute` now raise
|
|
19
|
+
`Cetustek::ResultError` on any code other than `A0` / `C0` instead of returning it
|
|
20
|
+
(e.g. `A2` 折讓金額大於原發票, `A7` 折讓日期早於發票日, `C2` 已申報無法作廢)
|
|
21
|
+
- **Breaking:** `AllowanceData` validates `tax_type`; only `1` 應稅 / `2` 零稅率 /
|
|
22
|
+
`3` 免稅 are valid on a 折讓單 (spec Table 15), the invoice-only `4`/`5`/`9` now
|
|
23
|
+
raise `ArgumentError`
|
|
24
|
+
- **Breaking:** `CreateAllowance.new(data, check_allowance: 1)` now raises
|
|
25
|
+
`ArgumentError`. The spec struck out the unconfirmed-allowance flag — 114/01/01 起
|
|
26
|
+
上傳的折讓單皆為已確認,只能送 `0`
|
|
27
|
+
|
|
8
28
|
## [0.6.0] - 2026-06-30
|
|
9
29
|
|
|
10
30
|
### Added
|
data/README.md
CHANGED
|
@@ -191,7 +191,30 @@ allowance = Cetustek::Models::AllowanceData.new(
|
|
|
191
191
|
)
|
|
192
192
|
Cetustek::CreateAllowance.new(allowance).execute # => "A0" on success
|
|
193
193
|
Cetustek::CancelAllowance.new('AA20240216000001', '明細錯誤').execute # => "C0" on success
|
|
194
|
-
Cetustek::QueryAllowance.
|
|
194
|
+
Cetustek::QueryAllowance.find('AA20240216000001') # parsed Hash, nil if unknown
|
|
195
|
+
Cetustek::QueryAllowance.query('AA20240216000001') # raw Savon response
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`tax_type` on an allowance only accepts `1` 應稅, `2` 零稅率 or `3` 免稅 — the
|
|
199
|
+
invoice-only values (`4`, `5`, `9`) raise `ArgumentError`. `unit_price` is
|
|
200
|
+
**tax-inclusive** (there is no `hastax` on allowances).
|
|
201
|
+
|
|
202
|
+
Any other result code raises `Cetustek::ResultError`, whose `#code` is the raw
|
|
203
|
+
code and whose message includes the documented reason (e.g. `A2 - 所有折讓金額加總
|
|
204
|
+
不能大於原發票金額`, `C2 - 折讓單已申報,無法作廢`).
|
|
205
|
+
|
|
206
|
+
`QueryAllowance.find` returns the Table 20 fields as snake_case symbols with the
|
|
207
|
+
line items under `:details`, values kept as the raw strings from the XML:
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
{ allowance_number: 'AA20240216000001', allowance_date: '2024/02/16',
|
|
211
|
+
invoice_number: 'AA10000000', invoice_date: '2024/02/14',
|
|
212
|
+
buyer_identifier: '12345678', buyer_name: '測試公司', buyer_address: nil,
|
|
213
|
+
reason: '退回', allowance_status: '開立', back_status: '已確認',
|
|
214
|
+
sale_amount: '95', tax_amount: '5',
|
|
215
|
+
details: [{ sequence_number: '1', product_code: '0001', description: '禮券',
|
|
216
|
+
quantity: '1', unit: '本', unit_price: '95', amount: '95',
|
|
217
|
+
tax: '5', tax_type: '1' }] }
|
|
195
218
|
```
|
|
196
219
|
|
|
197
220
|
### Mobile barcode validation (手機條碼)
|
|
@@ -4,8 +4,22 @@ require 'ox'
|
|
|
4
4
|
require 'cgi'
|
|
5
5
|
|
|
6
6
|
module Cetustek
|
|
7
|
-
# 2.10 CancelAllowance 作廢折讓單. Returns
|
|
7
|
+
# 2.10 CancelAllowance 作廢折讓單. Returns "C0" on success, raises ResultError otherwise.
|
|
8
8
|
class CancelAllowance
|
|
9
|
+
SUCCESS_CODE = 'C0'
|
|
10
|
+
|
|
11
|
+
# Spec AVM-26-03 Table 19.
|
|
12
|
+
RESULT_MESSAGES = {
|
|
13
|
+
'M:' => '欄位未填或格式錯誤',
|
|
14
|
+
'M1' => 'XML 格式錯誤',
|
|
15
|
+
'C1' => '上傳失敗',
|
|
16
|
+
'C2' => '折讓單已申報,無法作廢',
|
|
17
|
+
'C3' => '折讓單號不存在或須為已確認後的折讓單',
|
|
18
|
+
'C4' => '該作廢折讓單已經上傳',
|
|
19
|
+
'C5' => '折讓單已過作廢期限,無法作廢',
|
|
20
|
+
'Invalid' => '無效 IP,請通知系統商'
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
9
23
|
def initialize(allowance_number, reason)
|
|
10
24
|
@allowance_number = allowance_number
|
|
11
25
|
@reason = reason
|
|
@@ -13,7 +27,8 @@ module Cetustek
|
|
|
13
27
|
|
|
14
28
|
def execute
|
|
15
29
|
perform
|
|
16
|
-
@response.body[:cancel_allowance_response][:return]
|
|
30
|
+
ResultCode.check!(@response.body[:cancel_allowance_response][:return],
|
|
31
|
+
RESULT_MESSAGES, success: SUCCESS_CODE)
|
|
17
32
|
end
|
|
18
33
|
|
|
19
34
|
private
|
|
@@ -4,17 +4,45 @@ require 'ox'
|
|
|
4
4
|
require 'cgi'
|
|
5
5
|
|
|
6
6
|
module Cetustek
|
|
7
|
-
# 2.9 CreateAllowance 開立折讓單. Returns
|
|
8
|
-
# check_allowance: 0 = confirmed allowance (default), 1 = unconfirmed.
|
|
7
|
+
# 2.9 CreateAllowance 開立折讓單. Returns "A0" on success, raises ResultError otherwise.
|
|
9
8
|
class CreateAllowance
|
|
9
|
+
SUCCESS_CODE = 'A0'
|
|
10
|
+
|
|
11
|
+
# Spec AVM-26-03 Table 17.
|
|
12
|
+
RESULT_MESSAGES = {
|
|
13
|
+
'M:' => '欄位未填或格式錯誤',
|
|
14
|
+
'M1' => 'XML 格式錯誤',
|
|
15
|
+
'D0' => '沒有產品明細',
|
|
16
|
+
'D0_' => '產品編號格式錯誤',
|
|
17
|
+
'D1_' => '品名未填或格式錯誤',
|
|
18
|
+
'D2_' => '數量未填或格式錯誤',
|
|
19
|
+
'D3_' => '單價未填或格式錯誤',
|
|
20
|
+
'D4_' => '單位格式錯誤',
|
|
21
|
+
'D5_' => '數量*單價,其小計整數位大於 13 位',
|
|
22
|
+
'D999' => '明細筆數最多 9999 筆',
|
|
23
|
+
'A1' => '上傳失敗',
|
|
24
|
+
'A2' => '所有折讓金額加總不能大於原發票金額',
|
|
25
|
+
'A3' => '發票號碼不存在',
|
|
26
|
+
'A4' => '發票號碼已經被作廢',
|
|
27
|
+
'A5' => '折讓單已經上傳',
|
|
28
|
+
'A6' => '折讓總金額須大於零',
|
|
29
|
+
'A7' => '折讓日期應大於原發票開立日期',
|
|
30
|
+
'Invalid' => '無效 IP,請通知系統商'
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
# check_allowance 只剩 0(已確認的折讓單):規格 §2.9 已刪除 1,
|
|
34
|
+
# 114/01/01 起上傳的折讓單皆為已確認。
|
|
10
35
|
def initialize(allowance_data, check_allowance: 0)
|
|
36
|
+
raise ArgumentError, 'check_allowance must be 0 (114/01/01 起折讓單皆為已確認)' unless check_allowance.to_i.zero?
|
|
37
|
+
|
|
11
38
|
@data = allowance_data
|
|
12
|
-
@check_allowance =
|
|
39
|
+
@check_allowance = 0
|
|
13
40
|
end
|
|
14
41
|
|
|
15
42
|
def execute
|
|
16
43
|
perform
|
|
17
|
-
@response.body[:create_allowance_response][:return]
|
|
44
|
+
ResultCode.check!(@response.body[:create_allowance_response][:return],
|
|
45
|
+
RESULT_MESSAGES, success: SUCCESS_CODE)
|
|
18
46
|
end
|
|
19
47
|
|
|
20
48
|
private
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cetustek
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# Raised when an API call returns a result code other than the success code.
|
|
7
|
+
class ResultError < Error
|
|
8
|
+
attr_reader :code
|
|
9
|
+
|
|
10
|
+
def initialize(code, description = nil)
|
|
11
|
+
@code = code
|
|
12
|
+
super([code, description].compact.join(' - '))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module ResultCode
|
|
17
|
+
# Codes may carry a suffix naming the offending field or detail row
|
|
18
|
+
# ("M:AllowanceDate", "D2_3"), so an exact miss falls back to the table key
|
|
19
|
+
# without the suffix.
|
|
20
|
+
def self.describe(code, messages)
|
|
21
|
+
messages[code] || messages[code.to_s.sub(/(?<=[:_]).*\z/, '')]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns the code on success, otherwise raises ResultError.
|
|
25
|
+
def self.check!(code, messages, success:)
|
|
26
|
+
return code if code == success
|
|
27
|
+
|
|
28
|
+
raise ResultError.new(code, describe(code, messages))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -7,6 +7,10 @@ module Cetustek
|
|
|
7
7
|
# Request data for CreateAllowance (開立折讓單), spec AVM-26-03 Table 15/16.
|
|
8
8
|
# Line items reuse InvoiceItem (code/name/quantity/unit/unit_price).
|
|
9
9
|
class AllowanceData
|
|
10
|
+
# 折讓單稅別只有應稅/零稅率/免稅,發票的 4 (特種稅率)、5 (經海關出口)
|
|
11
|
+
# 與 9 (混合) 在折讓單無效。
|
|
12
|
+
TAX_TYPES = [TaxType::TAXABLE, TaxType::ZERO_RATE, TaxType::TAX_FREE].freeze
|
|
13
|
+
|
|
10
14
|
attr_reader :allowance_number, :allowance_date, :invoice_number,
|
|
11
15
|
:invoice_year, :buyer_address, :buyer_email, :tax_type,
|
|
12
16
|
:reason, :round_num, :items
|
|
@@ -19,6 +23,10 @@ module Cetustek
|
|
|
19
23
|
@buyer_address = attributes[:buyer_address]
|
|
20
24
|
@buyer_email = attributes[:buyer_email]
|
|
21
25
|
@tax_type = attributes[:tax_type] || TaxType::TAXABLE
|
|
26
|
+
unless TAX_TYPES.include?(@tax_type.to_i)
|
|
27
|
+
raise ArgumentError, "tax_type must be 1 (應稅), 2 (零稅率) or 3 (免稅), got #{@tax_type.inspect}"
|
|
28
|
+
end
|
|
29
|
+
|
|
22
30
|
@reason = attributes[:reason]
|
|
23
31
|
@round_num = attributes[:round_num] # optional 金額計算位數
|
|
24
32
|
@items = attributes[:items] || []
|
data/lib/cetustek/queries.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'ox'
|
|
4
|
+
|
|
3
5
|
module Cetustek
|
|
4
6
|
# Read-only SOAP queries. Each returns the raw Savon response, mirroring
|
|
5
7
|
# QueryInvoiceByOrderId. Spec AVM-26-03 §2.4 / §2.6 / §2.11.
|
|
@@ -48,6 +50,14 @@ module Cetustek
|
|
|
48
50
|
class QueryAllowance
|
|
49
51
|
extend Queries
|
|
50
52
|
|
|
53
|
+
# Spec AVM-26-03 Table 20. Note the query response uses ProductCode and
|
|
54
|
+
# InvoiceDate, unlike CreateAllowance's ProductionCode and InvoiceYear.
|
|
55
|
+
FIELDS = %w[AllowanceNumber AllowanceDate InvoiceNumber InvoiceDate
|
|
56
|
+
BuyerIdentifier BuyerName BuyerAddress Reason AllowanceStatus
|
|
57
|
+
BackStatus SaleAmount TaxAmount].freeze
|
|
58
|
+
DETAIL_FIELDS = %w[SequenceNumber ProductCode Description Quantity Unit
|
|
59
|
+
UnitPrice Amount Tax TaxType].freeze
|
|
60
|
+
|
|
51
61
|
def self.query(allowance_number)
|
|
52
62
|
soap_client.call(:query_allowance, message: {
|
|
53
63
|
allowancenumber: allowance_number,
|
|
@@ -55,5 +65,36 @@ module Cetustek
|
|
|
55
65
|
rentid: rentid
|
|
56
66
|
})
|
|
57
67
|
end
|
|
68
|
+
|
|
69
|
+
# Same query, with the returned XML parsed into a Hash of snake_case keys
|
|
70
|
+
# plus a :details array. Values are the raw strings from the XML; returns
|
|
71
|
+
# nil when the allowance number is unknown.
|
|
72
|
+
def self.find(allowance_number)
|
|
73
|
+
parse(query(allowance_number).body[:query_allowance_response][:return])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.parse(xml)
|
|
77
|
+
body = xml.to_s.strip
|
|
78
|
+
return nil if body.empty? || body == 'nodata'
|
|
79
|
+
|
|
80
|
+
root = Ox.parse(body)
|
|
81
|
+
root = root.root if root.is_a?(Ox::Document)
|
|
82
|
+
|
|
83
|
+
data = FIELDS.to_h { |field| [snake_case(field), text_of(root, field)] }
|
|
84
|
+
data[:details] = root.locate('Details/ProductItem').map do |item|
|
|
85
|
+
DETAIL_FIELDS.to_h { |field| [snake_case(field), text_of(item, field)] }
|
|
86
|
+
end
|
|
87
|
+
data
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.text_of(element, name)
|
|
91
|
+
element.locate(name).first&.text
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.snake_case(name)
|
|
95
|
+
name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private_class_method :text_of, :snake_case
|
|
58
99
|
end
|
|
59
100
|
end
|
data/lib/cetustek/version.rb
CHANGED
data/lib/cetustek.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "cetustek/version"
|
|
4
|
+
require_relative "cetustek/errors"
|
|
4
5
|
require_relative "cetustek/configuration"
|
|
5
6
|
require_relative "cetustek/models/allowance_data"
|
|
6
7
|
require_relative "cetustek/create_invoice"
|
|
@@ -12,8 +13,6 @@ require_relative "cetustek/cancel_allowance"
|
|
|
12
13
|
require_relative "cetustek/phone_barcode"
|
|
13
14
|
|
|
14
15
|
module Cetustek
|
|
15
|
-
class Error < StandardError; end
|
|
16
|
-
|
|
17
16
|
class << self
|
|
18
17
|
def configure
|
|
19
18
|
yield(config)
|
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.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Zac
|
|
@@ -84,6 +84,7 @@ files:
|
|
|
84
84
|
- lib/cetustek/configuration.rb
|
|
85
85
|
- lib/cetustek/create_allowance.rb
|
|
86
86
|
- lib/cetustek/create_invoice.rb
|
|
87
|
+
- lib/cetustek/errors.rb
|
|
87
88
|
- lib/cetustek/models/allowance_data.rb
|
|
88
89
|
- lib/cetustek/models/invoice_data.rb
|
|
89
90
|
- lib/cetustek/phone_barcode.rb
|
|
@@ -115,7 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
115
116
|
- !ruby/object:Gem::Version
|
|
116
117
|
version: '0'
|
|
117
118
|
requirements: []
|
|
118
|
-
rubygems_version: 4.0.
|
|
119
|
+
rubygems_version: 4.0.16
|
|
119
120
|
specification_version: 4
|
|
120
121
|
summary: A Ruby gem for handling electronic invoice operations
|
|
121
122
|
test_files: []
|