cetustek 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc27cec134bbdcde72328147677ace14f302f2423128209d8ec0ddd190ceeca7
4
- data.tar.gz: cfb7115017ef61ba4c928459f1d8d8815bf5a1fd616cc050a8be012a942aac3e
3
+ metadata.gz: 5134abbcc8b7229469df2b15f6efa704828f745e499f2609e04251bca8ed90c0
4
+ data.tar.gz: 50312823ef3166581b9518d97955ce8f839a80ad25e3a1f155fd48ea102a919d
5
5
  SHA512:
6
- metadata.gz: 1a350443ea05a452ec354e1f9d7a5cb624cd04087baecef9f756c715350497bdc6d9b0fbedc41ee2087d711aba66b72ecfbb7c207d239d8b8278266335a3eae4
7
- data.tar.gz: 5ac422d52d5d11b0bf71d97186ab9bd257f667ec135c942f87df934f66cb4f7653a40cda4794d62171aa72d905f98d60fe46e416409399e07abbfe679a063130
6
+ metadata.gz: b35ecc731affbcf77b48d858de62eae3e2e9dfe8f5bc328af30af2d6d9b27e51abe03de5633eedac8fc60106b9b50647b1c5b1a640c70e965b8a2267ff856685
7
+ data.tar.gz: 5192ff08c159704e14548beb39bfb132acb4bd929308ada3f882edbd18132fe1123997030792897102c77402e5a1aa7c11ea48a8015f967609b3121ee006a263
data/CHANGELOG.md CHANGED
@@ -5,6 +5,24 @@ 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.4.0] - 2026-06-29
9
+
10
+ ### Added
11
+ - `Cetustek::QueryInvoice.query(invoice_number, invoice_year)` — 查詢發票資訊 (§2.4)
12
+ - `Cetustek::QueryInvoiceNumberByOrderId.query(order_id)` — 以訂單編號查發票號碼 (§2.6)
13
+ - `Cetustek::CreateAllowance.new(allowance_data, check_allowance:).execute` — 開立折讓單 (§2.9)
14
+ - `Cetustek::CancelAllowance.new(allowance_number, reason).execute` — 作廢折讓單 (§2.10)
15
+ - `Cetustek::QueryAllowance.query(allowance_number)` — 查詢折讓資料 (§2.11)
16
+ - `Cetustek::PhoneBarcode.valid?(phone_code)` — 手機條碼驗證 (§3.1, HTTP/JSON, not SOAP)
17
+ - `Cetustek::Models::AllowanceData` value object and an optional `unit` on `InvoiceItem`
18
+
19
+ ### Changed
20
+ - `hastax` is now taken from the order via `InvoiceData.new(hastax:)` (default `1`,
21
+ tax-inclusive) instead of being hardcoded — e.g. tax-exclusive/tax-free purchases
22
+ can send `0`
23
+ - All generated XML is forced to UTF-8 encoding, preventing `Encoding::CompatibilityError`
24
+ from Savon when invoices/allowances contain Chinese text
25
+
8
26
  ## [0.3.0] - 2026-06-13
9
27
 
10
28
  ### Added
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Cetustek
2
2
 
3
+ [![RSpec Tests](https://github.com/7a6163/cetustek/actions/workflows/rspec.yml/badge.svg)](https://github.com/7a6163/cetustek/actions/workflows/rspec.yml)
4
+ [![codecov](https://codecov.io/gh/7a6163/cetustek/graph/badge.svg?token=N951Y9SE15)](https://codecov.io/gh/7a6163/cetustek)
5
+
3
6
  Cetustek is a Ruby gem designed for handling electronic invoice operations, including invoice cancellation. It communicates with the e-invoice system through SOAP Web Services.
4
7
 
5
8
  ## Features
@@ -130,10 +133,47 @@ Cetustek::Models::InvoiceItem.new(code: 'DISCOUNT', name: '折抵', quantity: 1,
130
133
  Cetustek::CancelInvoice.new(invoice).execute
131
134
  ```
132
135
 
133
- ### Query an Invoice by Order ID
136
+ ### Query invoices
137
+
138
+ ```ruby
139
+ Cetustek::QueryInvoiceByOrderId.query(order_id) # by order id
140
+ Cetustek::QueryInvoice.query(invoice_number, invoice_year) # by invoice number + year
141
+ Cetustek::QueryInvoiceNumberByOrderId.query(order_id) # just the invoice number
142
+ ```
143
+
144
+ ### Tax-inclusive vs tax-exclusive prices (`hastax`)
145
+
146
+ `hastax` comes from the order, not a fixed value: `1` (default) means the item
147
+ `unit_price`s already include tax; `0` means they are tax-exclusive (e.g. a tax-free
148
+ purchase). Set it on `InvoiceData`:
149
+
150
+ ```ruby
151
+ Cetustek::Models::InvoiceData.new(hastax: 0, items: [...])
152
+ ```
153
+
154
+ ### Allowances (折讓單)
155
+
156
+ ```ruby
157
+ allowance = Cetustek::Models::AllowanceData.new(
158
+ allowance_number: 'AA20240216000001',
159
+ allowance_date: Time.zone.today,
160
+ invoice_number: 'AA10000000',
161
+ invoice_year: '2024',
162
+ tax_type: 1,
163
+ reason: '退回',
164
+ items: [
165
+ Cetustek::Models::InvoiceItem.new(code: '0001', name: '禮券', quantity: 1, unit: '本', unit_price: 800)
166
+ ]
167
+ )
168
+ Cetustek::CreateAllowance.new(allowance).execute # => "A0" on success
169
+ Cetustek::CancelAllowance.new('AA20240216000001', '明細錯誤').execute # => "C0" on success
170
+ Cetustek::QueryAllowance.query('AA20240216000001')
171
+ ```
172
+
173
+ ### Mobile barcode validation (手機條碼)
134
174
 
135
175
  ```ruby
136
- response = Cetustek::QueryInvoiceByOrderId.query(order_id)
176
+ Cetustek::PhoneBarcode.valid?('/ABC123') # => true / false
137
177
  ```
138
178
 
139
179
  ## Development
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ox'
4
+ require 'cgi'
5
+
6
+ module Cetustek
7
+ # 2.10 CancelAllowance 作廢折讓單. Returns the result code (C0 = success).
8
+ class CancelAllowance
9
+ def initialize(allowance_number, reason)
10
+ @allowance_number = allowance_number
11
+ @reason = reason
12
+ end
13
+
14
+ def execute
15
+ perform
16
+ @response.body[:cancel_allowance_response][:return]
17
+ end
18
+
19
+ private
20
+
21
+ def perform
22
+ client = Savon.client(wsdl: Cetustek.config.url, open_timeout: 300, read_timeout: 300)
23
+ @response = client.call(:cancel_allowance, message: {
24
+ allowancexml: generate_xml,
25
+ source: Cetustek.config.site_id + Cetustek.config.password,
26
+ rentid: Cetustek.config.username
27
+ })
28
+ end
29
+
30
+ def generate_xml
31
+ doc = Ox::Document.new
32
+ instruct = Ox::Instruct.new(:xml)
33
+ instruct[:version] = '1.0'
34
+ instruct[:encoding] = 'UTF-8'
35
+ doc << instruct
36
+
37
+ allowance = Ox::Element.new('Allowance')
38
+ allowance[:XSDVersion] = '2.8'
39
+ doc << allowance
40
+
41
+ allowance << raw_tag('AllowanceNumber', @allowance_number)
42
+ allowance << raw_tag('Reason', @reason)
43
+
44
+ Ox.dump(doc).force_encoding('UTF-8')
45
+ end
46
+
47
+ def raw_tag(name, value)
48
+ Ox::Raw.new("<#{name}>#{CGI.escapeHTML(value.to_s)}</#{name}>")
49
+ end
50
+ end
51
+ end
@@ -42,7 +42,7 @@ module Cetustek
42
42
  invoice << Ox::Raw.new("<InvoiceNumber>#{@invoice.number}</InvoiceNumber>")
43
43
  invoice << Ox::Raw.new("<InvoiceYear>#{@invoice.created_at.year}</InvoiceYear>")
44
44
  invoice << Ox::Raw.new('<Remark>退貨</Remark>')
45
- @xml = Ox.dump(doc)
45
+ @xml = Ox.dump(doc).force_encoding('UTF-8')
46
46
  end
47
47
 
48
48
  def analize_response
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ox'
4
+ require 'cgi'
5
+
6
+ module Cetustek
7
+ # 2.9 CreateAllowance 開立折讓單. Returns the result code (A0 = success).
8
+ # check_allowance: 0 = confirmed allowance (default), 1 = unconfirmed.
9
+ class CreateAllowance
10
+ def initialize(allowance_data, check_allowance: 0)
11
+ @data = allowance_data
12
+ @check_allowance = check_allowance
13
+ end
14
+
15
+ def execute
16
+ perform
17
+ @response.body[:create_allowance_response][:return]
18
+ end
19
+
20
+ private
21
+
22
+ def perform
23
+ client = Savon.client(wsdl: Cetustek.config.url, open_timeout: 300, read_timeout: 300)
24
+ @response = client.call(:create_allowance, message: {
25
+ allowancexml: generate_xml,
26
+ checkallowance: @check_allowance,
27
+ source: Cetustek.config.site_id + Cetustek.config.password,
28
+ rentid: Cetustek.config.username
29
+ })
30
+ end
31
+
32
+ def generate_xml
33
+ doc = Ox::Document.new
34
+ instruct = Ox::Instruct.new(:xml)
35
+ instruct[:version] = '1.0'
36
+ instruct[:encoding] = 'UTF-8'
37
+ doc << instruct
38
+
39
+ allowance = Ox::Element.new('Allowance')
40
+ allowance[:XSDVersion] = '2.8'
41
+ doc << allowance
42
+
43
+ allowance << raw_tag('AllowanceNumber', @data.allowance_number)
44
+ allowance << raw_tag('AllowanceDate', @data.allowance_date.strftime('%Y/%m/%d'))
45
+ allowance << raw_tag('InvoiceNumber', @data.invoice_number)
46
+ allowance << raw_tag('InvoiceYear', @data.invoice_year)
47
+ allowance << raw_tag('BuyerAddress', @data.buyer_address)
48
+ allowance << raw_tag('BuyerEmailAddress', @data.buyer_email)
49
+ allowance << raw_tag('TaxType', @data.tax_type)
50
+ allowance << raw_tag('Reason', @data.reason)
51
+ allowance << raw_tag('RoundNum', @data.round_num) unless @data.round_num.nil?
52
+ allowance << build_details
53
+
54
+ Ox.dump(doc).force_encoding('UTF-8')
55
+ end
56
+
57
+ def build_details
58
+ details = Ox::Element.new('Details')
59
+ @data.items.each do |item|
60
+ product = Ox::Element.new('ProductItem')
61
+ product << raw_tag('ProductionCode', item.code)
62
+ product << raw_tag('Description', item.name)
63
+ product << raw_tag('Quantity', item.quantity)
64
+ product << raw_tag('Unit', item.unit)
65
+ product << raw_tag('UnitPrice', item.unit_price)
66
+ details << product
67
+ end
68
+ details
69
+ end
70
+
71
+ def raw_tag(name, value)
72
+ Ox::Raw.new("<#{name}>#{CGI.escapeHTML(value.to_s)}</#{name}>")
73
+ end
74
+ end
75
+ end
@@ -13,7 +13,7 @@ module Cetustek
13
13
 
14
14
  def execute
15
15
  xml = Services::InvoiceXmlBuilder.new(@invoice_data).build
16
- response = Services::InvoiceService.new(xml, @invoice_data.order_id).create
16
+ response = Services::InvoiceService.new(xml, @invoice_data.order_id, @invoice_data.hastax).create
17
17
  result = Services::ResponseHandler.new(response, @invoice_data, xml).process
18
18
 
19
19
  if defined?(Rails) && result[:number] && result[:random_number]
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'invoice_data'
4
+
5
+ module Cetustek
6
+ module Models
7
+ # Request data for CreateAllowance (開立折讓單), spec AVM-26-03 Table 15/16.
8
+ # Line items reuse InvoiceItem (code/name/quantity/unit/unit_price).
9
+ class AllowanceData
10
+ attr_reader :allowance_number, :allowance_date, :invoice_number,
11
+ :invoice_year, :buyer_address, :buyer_email, :tax_type,
12
+ :reason, :round_num, :items
13
+
14
+ def initialize(attributes = {})
15
+ @allowance_number = attributes[:allowance_number]
16
+ @allowance_date = attributes[:allowance_date]
17
+ @invoice_number = attributes[:invoice_number]
18
+ @invoice_year = attributes[:invoice_year]
19
+ @buyer_address = attributes[:buyer_address]
20
+ @buyer_email = attributes[:buyer_email]
21
+ @tax_type = attributes[:tax_type] || TaxType::TAXABLE
22
+ @reason = attributes[:reason]
23
+ @round_num = attributes[:round_num] # optional 金額計算位數
24
+ @items = attributes[:items] || []
25
+ end
26
+ end
27
+ end
28
+ end
@@ -19,7 +19,7 @@ module Cetustek
19
19
  attr_reader :order_id, :order_date, :buyer_identifier, :buyer_name,
20
20
  :buyer_email, :donate_mark, :carrier_type, :carrier_id,
21
21
  :carrier_id2, :npo_ban, :items, :payment_type,
22
- :tax_type, :tax_rate, :invoice_type
22
+ :tax_type, :tax_rate, :invoice_type, :hastax
23
23
 
24
24
  def initialize(attributes = {})
25
25
  @order_id = attributes[:order_id]
@@ -37,6 +37,9 @@ module Cetustek
37
37
  @tax_type = attributes[:tax_type] || TaxType::TAXABLE
38
38
  @tax_rate = attributes.fetch(:tax_rate, DEFAULT_TAX_RATE)
39
39
  @invoice_type = attributes[:invoice_type] || DEFAULT_INVOICE_TYPE
40
+ # hastax: 0 = item prices are tax-exclusive, 1 = tax-inclusive.
41
+ # Comes from the order (e.g. tax-free purchases), not a fixed value.
42
+ @hastax = attributes.fetch(:hastax, 1)
40
43
  end
41
44
 
42
45
  # 混合稅率發票 (限收銀機):每筆明細需標註 DType。
@@ -53,13 +56,14 @@ module Cetustek
53
56
  tax_free: 'TN' # 免稅商品
54
57
  }.freeze
55
58
 
56
- attr_reader :code, :name, :quantity, :unit_price, :tax_type
59
+ attr_reader :code, :name, :quantity, :unit_price, :tax_type, :unit
57
60
 
58
61
  def initialize(attributes = {})
59
62
  @code = attributes[:code]
60
63
  @name = attributes[:name]
61
64
  @quantity = attributes[:quantity]
62
65
  @unit_price = attributes[:unit_price]
66
+ @unit = attributes[:unit]
63
67
  @tax_type = attributes[:tax_type] || :taxable
64
68
  end
65
69
 
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ module Cetustek
8
+ # 3.1 手機條碼 API — validates a mobile barcode (手機條碼) against the
9
+ # 財政部 platform. Plain HTTP GET returning JSON, NOT the SOAP service.
10
+ class PhoneBarcode
11
+ ENDPOINT = 'https://api.cetustek.com.tw/PhoneBar.php'
12
+ AUTH_KEY = 'Cetus9Phone1API7' # spec: fixed for now, may become per-account
13
+
14
+ def self.valid?(phone_code)
15
+ new(phone_code).valid?
16
+ end
17
+
18
+ def initialize(phone_code)
19
+ @phone_code = phone_code
20
+ end
21
+
22
+ # true when the barcode exists (isExist == 'Y').
23
+ def valid?
24
+ response['isExist'] == 'Y'
25
+ end
26
+
27
+ # Full parsed JSON response: { "isExist", "code", "msg", "TxID", "version" }.
28
+ def response
29
+ uri = URI(ENDPOINT)
30
+ uri.query = URI.encode_www_form(
31
+ rentid: Cetustek.config.username,
32
+ authkey: AUTH_KEY,
33
+ phonecode: @phone_code
34
+ )
35
+ JSON.parse(Net::HTTP.get_response(uri).body)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cetustek
4
+ # Read-only SOAP queries. Each returns the raw Savon response, mirroring
5
+ # QueryInvoiceByOrderId. Spec AVM-26-03 §2.4 / §2.6 / §2.11.
6
+ module Queries
7
+ def soap_client
8
+ Savon.client(wsdl: Cetustek.config.url, open_timeout: 300, read_timeout: 300)
9
+ end
10
+
11
+ def source
12
+ Cetustek.config.site_id + Cetustek.config.password
13
+ end
14
+
15
+ def rentid
16
+ Cetustek.config.username
17
+ end
18
+ end
19
+
20
+ # 2.4 QueryInvoice 查詢發票資訊 (by invoice number + year)
21
+ class QueryInvoice
22
+ extend Queries
23
+
24
+ def self.query(invoice_number, invoice_year)
25
+ soap_client.call(:query_invoice, message: {
26
+ invoicenumber: invoice_number,
27
+ invoiceyear: invoice_year,
28
+ source: source,
29
+ rentid: rentid
30
+ })
31
+ end
32
+ end
33
+
34
+ # 2.6 QueryInvoiceNumberbyOrderid 以訂單編號查詢發票號碼
35
+ class QueryInvoiceNumberByOrderId
36
+ extend Queries
37
+
38
+ def self.query(order_id)
39
+ soap_client.call(:query_invoice_number_by_orderid, message: {
40
+ orderid: order_id,
41
+ source: source,
42
+ rentid: rentid
43
+ })
44
+ end
45
+ end
46
+
47
+ # 2.11 QueryAllowance 查詢折讓資料
48
+ class QueryAllowance
49
+ extend Queries
50
+
51
+ def self.query(allowance_number)
52
+ soap_client.call(:query_allowance, message: {
53
+ allowancenumber: allowance_number,
54
+ source: source,
55
+ rentid: rentid
56
+ })
57
+ end
58
+ end
59
+ end
@@ -6,9 +6,10 @@ require 'logger'
6
6
  module Cetustek
7
7
  module Services
8
8
  class InvoiceService
9
- def initialize(xml, order_id = nil)
9
+ def initialize(xml, order_id = nil, hastax = 1)
10
10
  @xml = xml
11
11
  @order_id = order_id
12
+ @hastax = hastax
12
13
  end
13
14
 
14
15
  def create
@@ -33,7 +34,7 @@ module Cetustek
33
34
  invoicexml: @xml,
34
35
  source: Cetustek.config.site_id + Cetustek.config.password,
35
36
  rentid: Cetustek.config.username,
36
- hastax: 1
37
+ hastax: @hastax
37
38
  })
38
39
  end
39
40
 
@@ -15,7 +15,7 @@ module Cetustek
15
15
  doc << create_xml_instruct
16
16
  doc << create_invoice_element
17
17
 
18
- Ox.dump(doc)
18
+ Ox.dump(doc).force_encoding('UTF-8')
19
19
  end
20
20
 
21
21
  private
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cetustek
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/cetustek.rb CHANGED
@@ -2,9 +2,14 @@
2
2
 
3
3
  require_relative "cetustek/version"
4
4
  require_relative "cetustek/configuration"
5
+ require_relative "cetustek/models/allowance_data"
5
6
  require_relative "cetustek/create_invoice"
6
7
  require_relative "cetustek/cancel_invoice"
7
8
  require_relative "cetustek/query_invoice_by_order_id"
9
+ require_relative "cetustek/queries"
10
+ require_relative "cetustek/create_allowance"
11
+ require_relative "cetustek/cancel_allowance"
12
+ require_relative "cetustek/phone_barcode"
8
13
 
9
14
  module Cetustek
10
15
  class Error < StandardError; end
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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zac
@@ -79,10 +79,15 @@ files:
79
79
  - README.md
80
80
  - Rakefile
81
81
  - lib/cetustek.rb
82
+ - lib/cetustek/cancel_allowance.rb
82
83
  - lib/cetustek/cancel_invoice.rb
83
84
  - lib/cetustek/configuration.rb
85
+ - lib/cetustek/create_allowance.rb
84
86
  - lib/cetustek/create_invoice.rb
87
+ - lib/cetustek/models/allowance_data.rb
85
88
  - lib/cetustek/models/invoice_data.rb
89
+ - lib/cetustek/phone_barcode.rb
90
+ - lib/cetustek/queries.rb
86
91
  - lib/cetustek/query_invoice_by_order_id.rb
87
92
  - lib/cetustek/services/invoice_service.rb
88
93
  - lib/cetustek/services/invoice_xml_builder.rb