paid_tw-ubn 0.1.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 +7 -0
- data/LICENSE +45 -0
- data/README.md +62 -0
- data/lib/paid_tw/ubn/version.rb +7 -0
- data/lib/paid_tw/ubn.rb +216 -0
- metadata +49 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1235c21850cf6463a28a5ffaebf881de7c52e76019319883414ca2d0b694a0b8
|
|
4
|
+
data.tar.gz: d1c2e35153e2cbfb248e616290f28b1142d4d8719fb8c66e6345e9dc44b34409
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3bb51f144ba623cb7d548a88a81d7b323b1deeae215f40ec16d565b1dfabe09c3b02262d8cd261005a9984012f7381625fd378db9b1053a2ba363efd1c6518ce
|
|
7
|
+
data.tar.gz: f0db2095e1a65c3b9fb62d4d233dd514ae31b4aee1a25433a3ae0b99ec874a260ae56af410a7c05c927803b176bbec993bf892c4f0f8d1d2308acf58094bb56d
|
data/LICENSE
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 paid.tw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
NOTE: This MIT license covers the SOURCE CODE in this repository only.
|
|
26
|
+
|
|
27
|
+
The published DATA artifacts are derived from open data released by
|
|
28
|
+
財政部財政資訊中心 under 政府資料開放授權條款-第1版 (Open Government Data
|
|
29
|
+
License v1, https://data.gov.tw/license). Failing to attach the 顯名聲明
|
|
30
|
+
below means the licence was not granted.
|
|
31
|
+
|
|
32
|
+
提供機關/單位:財政部財政資訊中心
|
|
33
|
+
開放資料:
|
|
34
|
+
- 全國營業(稅籍)登記資料集(https://data.gov.tw/dataset/9400,BGMOPEN1.zip)
|
|
35
|
+
- 全國營業(稅籍)登記(停業)資料集(https://data.gov.tw/dataset/75140,BGMOPEN1X.csv)
|
|
36
|
+
版本:each build's extract date, recorded in manifest.json as dataDate and
|
|
37
|
+
attribution.datasets[].version (for example 28-OCT-26).
|
|
38
|
+
|
|
39
|
+
此開放資料依政府資料開放授權條款 (Open Government Data License) 進行公眾釋出,使用者於遵守本條款各項規定之前提下,得利用之。
|
|
40
|
+
|
|
41
|
+
政府資料開放授權條款:https://data.gov.tw/license
|
|
42
|
+
|
|
43
|
+
See docs/data-dictionary.md. Per-record GCIS responses, when used, are a
|
|
44
|
+
separate government source under the same licence and need their own
|
|
45
|
+
attribution; this repository does not republish a GCIS dump.
|
data/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# paid_tw-ubn
|
|
2
|
+
|
|
3
|
+
The Ruby counterpart of the [`@paid-tw/ubn`](https://www.npmjs.com/package/@paid-tw/ubn)
|
|
4
|
+
npm client, for the server side of a checkout: validate a Taiwan Unified
|
|
5
|
+
Business Number (統一編號), compare an invoice title (抬頭) with the registered
|
|
6
|
+
name, and look the registered name up through a deployed
|
|
7
|
+
[tw-ubn](https://github.com/paid-tw/tw-ubn) Worker.
|
|
8
|
+
|
|
9
|
+
Pure Ruby, no runtime dependencies, Ruby 3.2 or later.
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# Gemfile
|
|
13
|
+
gem 'paid_tw-ubn'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require 'paid_tw/ubn'
|
|
18
|
+
|
|
19
|
+
PaidTw::Ubn.valid?('22099131') # => true (post-2023 rule: sum divisible by 5)
|
|
20
|
+
|
|
21
|
+
title = PaidTw::Ubn.lookup_title('22099131', base_url: 'https://ubn.example.com', timeout: 2)
|
|
22
|
+
|
|
23
|
+
if title.nil?
|
|
24
|
+
# No answer: bad checksum, timeout, network error, non-2xx, or a name that
|
|
25
|
+
# cannot be used as a title. Treat the UBN as not looked up.
|
|
26
|
+
elsif !title.found?
|
|
27
|
+
# No public source has this UBN. Not proof that it is invalid.
|
|
28
|
+
else
|
|
29
|
+
title.name # => "台灣積體電路製造股份有限公司"
|
|
30
|
+
title.status # => "active", "suspended", "closed" or nil (unknown)
|
|
31
|
+
PaidTw::Ubn.same_title?(typed_title, title.name)
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Ruby | npm client |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `PaidTw::Ubn.valid?(ban)` | `validateUBN(ban)` |
|
|
38
|
+
| `PaidTw::Ubn.lookup_title(ban, base_url:, timeout:)` | `lookupTitle(ban, { baseUrl, timeoutMs })` |
|
|
39
|
+
| `PaidTw::Ubn.same_title?(a, b)` | `isSameTitle(a, b)` |
|
|
40
|
+
| `PaidTw::Ubn.private_use?(title)` | `hasPrivateUse(title)` |
|
|
41
|
+
|
|
42
|
+
`lookup_title` never raises. `timeout` is in seconds and bounds the connection
|
|
43
|
+
and each read, not the whole call; the request is not retried. A found `Title`
|
|
44
|
+
also carries `ban`, `address`, `use_invoice`, `kind`, `source`,
|
|
45
|
+
`company_status`, `closed_date` and `setup_date`, each nil when the Worker did
|
|
46
|
+
not send it. What a lookup result means, and why "not found" is not "invalid",
|
|
47
|
+
is in the [repository README](https://github.com/paid-tw/tw-ubn#readme).
|
|
48
|
+
|
|
49
|
+
`same_title?` ignores full- vs half-width forms (NFKC), 臺 vs 台, whitespace and
|
|
50
|
+
Latin case. It never strips or abbreviates a legal form such as 股份有限公司.
|
|
51
|
+
|
|
52
|
+
The shard reader (`lookupVAT`, `shardOf`) and the direct FIA / GCIS clients of
|
|
53
|
+
the npm package are not ported: a server should ask the Worker, which caches.
|
|
54
|
+
|
|
55
|
+
## Develop
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cd ruby && bundle install && bundle exec rake test
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The tests read the vectors in [`../fixtures`](../fixtures), which the npm
|
|
62
|
+
client's tests read too, and the Rust core's sample of real UBNs.
|
data/lib/paid_tw/ubn.rb
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
|
|
7
|
+
require_relative 'ubn/version'
|
|
8
|
+
|
|
9
|
+
module PaidTw
|
|
10
|
+
# Taiwan Unified Business Number (UBN/BAN) helpers: checksum
|
|
11
|
+
# validation, invoice-title comparison, and a title lookup against a deployed
|
|
12
|
+
# tw-ubn Worker. The Ruby counterpart of the `@paid-tw/ubn` npm client; the
|
|
13
|
+
# two are held together by the vectors in `fixtures/`.
|
|
14
|
+
module Ubn
|
|
15
|
+
WEIGHTS = [1, 2, 1, 2, 1, 2, 4, 1].freeze
|
|
16
|
+
|
|
17
|
+
# What ECMAScript's `\s` matches, so that a title compares the same here as
|
|
18
|
+
# in the npm client. Ruby's own `\s` is ASCII-only and `[[:space:]]` differs
|
|
19
|
+
# on U+0085 and U+FEFF.
|
|
20
|
+
WHITESPACE = "\t\n\v\f\r -
"
|
|
21
|
+
WHITESPACE_RUN = /[#{WHITESPACE}]+/
|
|
22
|
+
PRIVATE_USE = /\p{Co}/
|
|
23
|
+
|
|
24
|
+
# A capital sigma that ends a word: a cased letter before it and none after
|
|
25
|
+
# it, case-ignorable characters (combining marks, full stops, apostrophes,
|
|
26
|
+
# ...) skipped on both sides. ECMAScript's `toLowerCase` gives it the final
|
|
27
|
+
# form "ς"; Ruby's `downcase` knows no context and gives "σ". A character
|
|
28
|
+
# that is both cased and case-ignorable counts as ignorable, as JavaScript
|
|
29
|
+
# engines have it.
|
|
30
|
+
CASED = '[\p{Cased}&&\P{Case_Ignorable}]'
|
|
31
|
+
FINAL_SIGMA = /(#{CASED}\p{Case_Ignorable}*)Σ(?!\p{Case_Ignorable}*#{CASED})/
|
|
32
|
+
|
|
33
|
+
STATUSES = %w[active suspended closed].freeze
|
|
34
|
+
KINDS = %w[business nonprofit school agency].freeze
|
|
35
|
+
SOURCES = %w[dataset fia-api gcis].freeze
|
|
36
|
+
|
|
37
|
+
DEFAULT_TIMEOUT = 2
|
|
38
|
+
SUCCESS = (200..299)
|
|
39
|
+
|
|
40
|
+
# The answer of `lookup_title`. `found?` is false when the Worker answered
|
|
41
|
+
# and no source it consulted has a record; every other field is then nil.
|
|
42
|
+
# A nil `status` means unknown, not active.
|
|
43
|
+
Title = Data.define(
|
|
44
|
+
:found, :ban, :name, :status, :address, :use_invoice, :kind, :source,
|
|
45
|
+
:company_status, :closed_date, :setup_date
|
|
46
|
+
) do
|
|
47
|
+
def initialize(found:, ban: nil, name: nil, status: nil, address: nil, use_invoice: nil,
|
|
48
|
+
kind: nil, source: nil, company_status: nil, closed_date: nil, setup_date: nil)
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def found? = found
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
NOT_FOUND = Title.new(found: false)
|
|
56
|
+
|
|
57
|
+
module_function
|
|
58
|
+
|
|
59
|
+
# True when `ban` is a well-formed 8-digit UBN under the post-2023 rule
|
|
60
|
+
# (weighted digit sum divisible by 5). It says nothing about whether the
|
|
61
|
+
# number is registered. Never raises: a value that is not a string, or a
|
|
62
|
+
# string whose bytes are invalid in its own encoding, is false.
|
|
63
|
+
def valid?(ban)
|
|
64
|
+
# A regexp raises on a string it cannot read: one with invalid bytes, or
|
|
65
|
+
# in an encoding that is not ASCII-compatible (UTF-16, UTF-32).
|
|
66
|
+
return false unless ban.is_a?(String) && ban.encoding.ascii_compatible? && ban.valid_encoding?
|
|
67
|
+
return false unless ban.match?(/\A[0-9]{8}\z/)
|
|
68
|
+
|
|
69
|
+
sum = ban.each_char.with_index.sum do |char, index|
|
|
70
|
+
product = char.to_i * WEIGHTS[index]
|
|
71
|
+
(product / 10) + (product % 10)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
(sum % 5).zero? || (ban[6] == '7' && ((sum + 1) % 5).zero?)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# True when `title` contains a Unicode Private Use Area character. The tax
|
|
78
|
+
# registry writes some names with locally-defined glyphs mapped there; they
|
|
79
|
+
# render as boxes and an e-invoice platform rejects them.
|
|
80
|
+
def private_use?(title)
|
|
81
|
+
utf8(title).match?(PRIVATE_USE)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# True when two invoice titles name the same entity once incidental typing
|
|
85
|
+
# differences are ignored: full- vs half-width forms (NFKC), the two
|
|
86
|
+
# spellings of "tai" (U+81FA and U+53F0), all whitespace, and Latin case.
|
|
87
|
+
# Legal forms are never stripped or abbreviated ("Co., Ltd." written in
|
|
88
|
+
# full is not its bracketed short form), and an empty title matches nothing.
|
|
89
|
+
# Neither does a title with bytes that have no reading as UTF-8: a string
|
|
90
|
+
# in another encoding is compared by its characters, one that is broken or
|
|
91
|
+
# binary with non-ASCII bytes is not compared at all.
|
|
92
|
+
def same_title?(a, b)
|
|
93
|
+
normalized = normalize_title(a)
|
|
94
|
+
!normalized.empty? && normalized == normalize_title(b)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Looks up the registered name of `ban` through a deployed tw-ubn Worker
|
|
98
|
+
# (`GET <base_url>/v1/vat/<ban>`). Never raises. Three outcomes:
|
|
99
|
+
#
|
|
100
|
+
# - a `Title` with `found?` true: a registration with a usable name. Its
|
|
101
|
+
# `status` may be "suspended" or "closed"; what to do then is the
|
|
102
|
+
# caller's call.
|
|
103
|
+
# - `NOT_FOUND`: the Worker answered and no source has a record. This is
|
|
104
|
+
# not proof of an invalid UBN: many withholding-unit UBNs (clinics,
|
|
105
|
+
# professional practices, ...) have no public record.
|
|
106
|
+
# - nil: no answer. A malformed UBN (no request is made), a timeout, a
|
|
107
|
+
# network error, any non-2xx response, a body that is not the expected
|
|
108
|
+
# JSON, a `ban` in the body other than the one asked for, or a name that
|
|
109
|
+
# is empty or carries Private Use Area glyphs.
|
|
110
|
+
#
|
|
111
|
+
# `timeout` is in seconds and bounds the connection and each read, not the
|
|
112
|
+
# whole call. `http` replaces the transport: a callable taking the URL and
|
|
113
|
+
# the timeout, returning `[status, body]`.
|
|
114
|
+
def lookup_title(ban, base_url:, timeout: DEFAULT_TIMEOUT, http: nil)
|
|
115
|
+
return nil unless valid?(ban)
|
|
116
|
+
|
|
117
|
+
url = "#{base_url.to_s.sub(%r{/+\z}, '')}/v1/vat/#{ban}"
|
|
118
|
+
status, body = (http || method(:get)).call(url, timeout)
|
|
119
|
+
return nil unless SUCCESS.cover?(status)
|
|
120
|
+
|
|
121
|
+
parse(JSON.parse(body), ban)
|
|
122
|
+
rescue StandardError
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def get(url, timeout)
|
|
127
|
+
uri = URI.parse(url)
|
|
128
|
+
raise ArgumentError, "not an http(s) URL: #{url}" unless uri.is_a?(URI::HTTP)
|
|
129
|
+
|
|
130
|
+
options = {
|
|
131
|
+
use_ssl: uri.scheme == 'https',
|
|
132
|
+
open_timeout: timeout,
|
|
133
|
+
read_timeout: timeout,
|
|
134
|
+
write_timeout: timeout,
|
|
135
|
+
# Net::HTTP retries an idempotent request once, which would double the
|
|
136
|
+
# time a dead Worker can hold the caller.
|
|
137
|
+
max_retries: 0
|
|
138
|
+
}
|
|
139
|
+
Net::HTTP.start(uri.host, uri.port, **options) do |connection|
|
|
140
|
+
connection.request_get(uri.request_uri) do |response|
|
|
141
|
+
status = response.code.to_i
|
|
142
|
+
# The status is all a non-2xx response has to say. Returning from
|
|
143
|
+
# inside the block leaves before Net::HTTP reads the body, and closes
|
|
144
|
+
# the connection: an error body that stalls or trickles holds no one.
|
|
145
|
+
return [status, nil] unless SUCCESS.cover?(status)
|
|
146
|
+
|
|
147
|
+
return [status, response.read_body]
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def parse(body, ban)
|
|
153
|
+
return nil unless body.is_a?(Hash)
|
|
154
|
+
|
|
155
|
+
found = body['found']
|
|
156
|
+
return nil unless [true, false].include?(found)
|
|
157
|
+
return nil if body.key?('ban') && body['ban'] != ban
|
|
158
|
+
return NOT_FOUND unless found
|
|
159
|
+
|
|
160
|
+
name = body['name']
|
|
161
|
+
return nil unless name.is_a?(String) && !name.gsub(WHITESPACE_RUN, '').empty?
|
|
162
|
+
return nil if body['nameHasPrivateUse'] == true || private_use?(name)
|
|
163
|
+
|
|
164
|
+
Title.new(
|
|
165
|
+
found: true,
|
|
166
|
+
ban: ban,
|
|
167
|
+
name: name,
|
|
168
|
+
status: one_of(body['status'], STATUSES),
|
|
169
|
+
address: string(body['address']),
|
|
170
|
+
use_invoice: [true, false].include?(body['useInvoice']) ? body['useInvoice'] : nil,
|
|
171
|
+
kind: one_of(body['kind'], KINDS),
|
|
172
|
+
source: one_of(body['source'], SOURCES),
|
|
173
|
+
company_status: string(body['companyStatus']),
|
|
174
|
+
closed_date: string(body['closedDate']),
|
|
175
|
+
setup_date: string(body['setupDate'])
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# A title that has no reading as UTF-8 normalizes to the empty title, which
|
|
180
|
+
# matches nothing. Replacing its unreadable bytes instead would turn each
|
|
181
|
+
# into the same U+FFFD and make different titles compare equal.
|
|
182
|
+
def normalize_title(title)
|
|
183
|
+
text = title.to_s.encode(Encoding::UTF_8)
|
|
184
|
+
return '' unless text.valid_encoding?
|
|
185
|
+
|
|
186
|
+
text
|
|
187
|
+
.unicode_normalize(:nfkc)
|
|
188
|
+
.tr('臺', '台')
|
|
189
|
+
.gsub(WHITESPACE_RUN, '')
|
|
190
|
+
.then { |stripped| lowercase(stripped) }
|
|
191
|
+
rescue EncodingError
|
|
192
|
+
''
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# `String#downcase` as ECMAScript's `toLowerCase` has it: see FINAL_SIGMA.
|
|
196
|
+
def lowercase(text)
|
|
197
|
+
return text.downcase unless text.include?('Σ')
|
|
198
|
+
|
|
199
|
+
text.gsub(FINAL_SIGMA, '\1ς').downcase
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def utf8(value)
|
|
203
|
+
value.to_s.encode(Encoding::UTF_8, invalid: :replace, undef: :replace)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def one_of(value, allowed)
|
|
207
|
+
value if allowed.include?(value)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def string(value)
|
|
211
|
+
value if value.is_a?(String)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private_class_method :get, :parse, :normalize_title, :lowercase, :utf8, :one_of, :string
|
|
215
|
+
end
|
|
216
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: paid_tw-ubn
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- paid-tw
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'The Ruby counterpart of the @paid-tw/ubn npm client: validate a Taiwan
|
|
13
|
+
Unified Business Number, compare an invoice title with the registered name, and
|
|
14
|
+
look the registered name up through a deployed tw-ubn Worker.'
|
|
15
|
+
executables: []
|
|
16
|
+
extensions: []
|
|
17
|
+
extra_rdoc_files: []
|
|
18
|
+
files:
|
|
19
|
+
- LICENSE
|
|
20
|
+
- README.md
|
|
21
|
+
- lib/paid_tw/ubn.rb
|
|
22
|
+
- lib/paid_tw/ubn/version.rb
|
|
23
|
+
homepage: https://github.com/paid-tw/tw-ubn
|
|
24
|
+
licenses:
|
|
25
|
+
- MIT
|
|
26
|
+
metadata:
|
|
27
|
+
source_code_uri: https://github.com/paid-tw/tw-ubn/tree/main/ruby
|
|
28
|
+
changelog_uri: https://github.com/paid-tw/tw-ubn/blob/main/CHANGELOG.md
|
|
29
|
+
bug_tracker_uri: https://github.com/paid-tw/tw-ubn/issues
|
|
30
|
+
rubygems_mfa_required: 'true'
|
|
31
|
+
rdoc_options: []
|
|
32
|
+
require_paths:
|
|
33
|
+
- lib
|
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '3.2'
|
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '0'
|
|
44
|
+
requirements: []
|
|
45
|
+
rubygems_version: 3.6.9
|
|
46
|
+
specification_version: 4
|
|
47
|
+
summary: Taiwan business number (統一編號) checksum, invoice-title comparison and title
|
|
48
|
+
lookup
|
|
49
|
+
test_files: []
|