einvoice 1.3 → 1.5.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: 8073afaa58225b965eeed8fb135712ab8b1b76c95b759324f35e875bf9eacb32
4
- data.tar.gz: ed9440ffcba86cdbf84e98b0030fb3e53c9e1b7418551c99cd102639c4921712
3
+ metadata.gz: 0ce2293f378b646e5ae9ab94d5cce8b539a7dd441d8db03472ae3c6f4bbd9735
4
+ data.tar.gz: 0d8e775b71d10cb37f3892cc40bd8e46e1ce872721ec7cb1c0864807d6a5f24d
5
5
  SHA512:
6
- metadata.gz: f0d297e2b70749e83b4ebf531ee49597d72ee6404a295eb3abd03c6e68e06da500cbbdfc6fe556fb8be761786b1ef4a9b11626e7e5758eda12f0cbf13420ba1d
7
- data.tar.gz: 75ca49a608cbd45bcc1fdc8575183d98ab079ec18684e073da6e944fc77f63eaa072e9e87d32e360ee8bda102dcccc9ed9b01fb3853d7a842d67368cd7de0f48
6
+ metadata.gz: 233c9f4194869b5e61420c7b255f768befe0f09cb3f952f1c300c7d9b8e417d98053461f1b794cf5f7a9a7c4b4a428b38b99db0005e77429d9686bf3afad1bda
7
+ data.tar.gz: 11f5ba7e0132eb0dc9eb4d0fc16cd48252b950b3c189f2c45efa4757bfbc43936c3c97155692d2e3b989bf038e528e20f5ee023031817fed9ab0a3a7bbb564d7
data/.coderabbit.yaml ADDED
@@ -0,0 +1,14 @@
1
+ reviews:
2
+ profile: chill
3
+ request_changes_workflow: false
4
+ auto_review:
5
+ enabled: true
6
+ drafts: false
7
+ path_filters:
8
+ - "!lib/einvoice/donation_unit_list.json"
9
+ path_instructions:
10
+ - path: "lib/einvoice/tradevan/**"
11
+ instructions: >
12
+ This is legacy 1.x code kept for backward compatibility.
13
+ Do not suggest large-scale refactors or architecture changes here.
14
+ Focus only on correctness bugs and security issues.
@@ -2,9 +2,9 @@ name: build
2
2
 
3
3
  on:
4
4
  push:
5
- branches: [ main ]
5
+ branches: [ 1-x-stable ]
6
6
  pull_request:
7
- branches: [ main ]
7
+ branches: [ 1-x-stable ]
8
8
 
9
9
  permissions:
10
10
  contents: read
@@ -16,7 +16,7 @@ jobs:
16
16
  ruby-version: ['3.1', '3.2', '3.3', '3.4']
17
17
  runs-on: ubuntu-latest
18
18
  steps:
19
- - uses: actions/checkout@v3
19
+ - uses: actions/checkout@v4
20
20
  - name: Set up Ruby
21
21
  uses: ruby/setup-ruby@v1
22
22
  with:
@@ -30,7 +30,10 @@ jobs:
30
30
  - name: Run tests
31
31
  run: bundle exec rspec
32
32
 
33
- - name: Publish code coverage
34
- uses: paambaati/codeclimate-action@v9
35
- env:
36
- CC_TEST_REPORTER_ID: 134e1f1cc8b60db4f2a491152c9227a6dbbfbeba5c1d05ee8b44cd78a1069f28
33
+ - name: Upload coverage to Codecov
34
+ if: matrix.ruby-version == '3.4'
35
+ uses: codecov/codecov-action@v5
36
+ with:
37
+ token: ${{ secrets.CODECOV_TOKEN }}
38
+ files: ./coverage/lcov.info
39
+ fail_ci_if_error: true
@@ -2,17 +2,30 @@ name: Daily update
2
2
  on:
3
3
  schedule:
4
4
  - cron: 0 0 * * *
5
+ workflow_dispatch:
6
+
7
+ permissions:
8
+ contents: write
9
+
5
10
  jobs:
6
11
  build:
7
- runs-on: ubuntu-22.04
12
+ runs-on: ubuntu-latest
8
13
  steps:
9
- - uses: actions/checkout@v3
10
- - uses: actions/setup-ruby@v1
11
- - name: bundle and run rake update
14
+ - uses: actions/checkout@v4
15
+ - uses: ruby/setup-ruby@v1
16
+ with:
17
+ ruby-version: '3.3'
18
+ bundler-cache: true
19
+ - name: Update donation unit list
20
+ run: bundle exec rake update_donation_unit
21
+ - name: Commit and push changes
12
22
  run: |
13
- bundle install --jobs 4 --retry 3
14
- bundle exec rake update_donation_unit
15
- - uses: mikeal/publish-to-github-action@master
16
- env:
17
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
18
- BRANCH_NAME: 'main'
23
+ if git diff --quiet; then
24
+ echo "Donation unit list is up to date"
25
+ exit 0
26
+ fi
27
+ git config user.name "github-actions[bot]"
28
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
29
+ git add lib/einvoice/donation_unit_list.json
30
+ git commit -m "Automated donation unit list update"
31
+ git push
data/CHANGELOG.md ADDED
@@ -0,0 +1,74 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [1.5.0] - 2026-09-11
9
+
10
+ ### Fixed
11
+ - Tradevan `IssueData#payload` and `VoidData#payload` now exclude
12
+ `context_for_validation`. On Rails 8 this internal ivar was sent as a
13
+ business field once `valid?` had run, and Tradevan rejected the whole
14
+ request. Thanks @dlackty.
15
+
16
+ ### Changed
17
+ - **Ruby 3.1 or newer is now required.** CI covers 3.1 through 3.4.
18
+ - **Faraday 2 is now required** (`~> 2.1`); `faraday_middleware` is gone,
19
+ replaced by a built-in `Faraday::Response::ParseXml`. If you pin
20
+ `faraday` to 1.x, stay on 1.4.0.
21
+ - `Einvoice::Utils` and the `gyoku` dependency have been removed. Nothing
22
+ in the gem called them.
23
+
24
+ ### Internal
25
+ - Added request-level Tradevan provider specs.
26
+ - CI runs on `1-x-stable`; the donation unit list task parses with `CSV`.
27
+ - README documents the allowance operations and 1.x maintenance mode.
28
+
29
+ ## [1.4.0] - 2026-08-08
30
+
31
+ ### Changed
32
+ - **TLS certificate verification is now enabled by default.** Previously every
33
+ request was sent with `ssl: { verify: false }`, regardless of configuration.
34
+ If you were relying on that behavior (e.g. against an endpoint with an
35
+ incomplete certificate chain), set `config.ssl_verify = false` or pass
36
+ `ssl_verify: false` to `Provider.new` to restore the old behavior.
37
+ - `Client` now delegates to the provider via `public_send` instead of `send`,
38
+ and implements `respond_to_missing?`. Private provider methods (e.g.
39
+ `encrypt`, `connection`) are no longer reachable through `Client`.
40
+
41
+ ### Fixed
42
+ - `TotalValidator` comparison was inverted and compared a `String` against an
43
+ `Integer`, so it never actually validated anything. It now correctly fails
44
+ when `total` doesn't match the sum of item totals.
45
+ - `AllowanceNumberValidator` and `IssueItem#invoiceTime`'s format regexp had
46
+ broken escape sequences (`\A` and `\d` collapsing to literal characters in a
47
+ double-quoted string), causing them to reject all valid input.
48
+ - `ItemListValidator` pushed directly onto `record.errors[:itemList]`, which
49
+ is deprecated in Rails 6.1 and removed in 7.0. It now uses `errors.add` and
50
+ actually registers errors again.
51
+ - Tradevan response decoding used `JSON.load`, which is unsafe for untrusted
52
+ input. Switched to `JSON.parse`.
53
+ - `lib/einvoice/donation_unit_list.json` was stale since 2023-12 (the
54
+ scheduled update workflow had been silently broken), causing
55
+ `DonationUnitValidator` to reject currently valid donation codes and accept
56
+ retired ones. The list has been refreshed (~340 entries changed).
57
+
58
+ ### Added
59
+ - Specs for `Client` delegation, `Configuration#ssl_verify`,
60
+ `Provider#connection` SSL behavior, the validator fixes above, and the
61
+ previously-untested `decode_tradevan` middleware.
62
+
63
+ ### Internal
64
+ - CI coverage reporting migrated from Code Climate (discontinued) to Codecov.
65
+ - The scheduled `cron.yml` workflow that refreshes the donation unit list
66
+ has been repaired — it depended on two archived GitHub Actions
67
+ (`actions/setup-ruby`, `mikeal/publish-to-github-action`) and hadn't run
68
+ successfully since 2023-12.
69
+
70
+ ---
71
+
72
+ Versions prior to 1.4.0 were not tracked in this file. See the
73
+ [release history](https://github.com/abookyun/einvoice/releases) and
74
+ [commit log](https://github.com/abookyun/einvoice/commits/main/) for earlier changes.
data/Gemfile CHANGED
@@ -3,7 +3,14 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in einvoice.gemspec
4
4
  gemspec
5
5
 
6
- # Integrate CodeClimate Code Coverage into TravisCI
7
- # https://github.com/codeclimate/test-reporter/issues/413
8
- gem 'simplecov', require: false, group: :test
9
- gem "simplecov_json_formatter", require: false, group: :test
6
+ # Remove this pin once we drop Ruby 3.1 support. json 3.0 dropped quirks_mode,
7
+ # which activesupport 7.2.3.2 still passes; 7.2.3.2 is the newest activesupport
8
+ # that works on Ruby 3.1.
9
+ gem "json", "< 3.0"
10
+
11
+ # Used by the update_donation_unit rake task; csv leaves the default gems in Ruby 3.4.
12
+ gem "csv", require: false, group: :development
13
+
14
+ gem "ostruct", require: false, group: :development
15
+ gem "simplecov", require: false, group: :test
16
+ gem "simplecov-lcov", require: false, group: :test
data/README.md CHANGED
@@ -1,7 +1,11 @@
1
1
  # Einvoice
2
2
  [![Build Status](https://github.com/abookyun/einvoice/actions/workflows/build.yml/badge.svg)](https://github.com/abookyun/einvoice/actions/workflows/build.yml)
3
- [![Code Climate](https://codeclimate.com/github/abookyun/einvoice/badges/gpa.svg)](https://codeclimate.com/github/abookyun/einvoice)
4
- [![Test Coverage](https://codeclimate.com/github/abookyun/einvoice/badges/coverage.svg)](https://codeclimate.com/github/abookyun/einvoice/coverage)
3
+ [![codecov](https://codecov.io/github/abookyun/einvoice/graph/badge.svg?token=yxJNmXiUyp)](https://codecov.io/github/abookyun/einvoice)
4
+
5
+ > **The 1.x line is moving to maintenance mode.** A provider-agnostic 2.0
6
+ > rewrite is in progress on `main`; once it lands, 1.x continues on the
7
+ > [`1-x-stable`](https://github.com/abookyun/einvoice/tree/1-x-stable) branch.
8
+ > If you depend on the current Tradevan client, pin `~> 1.4`.
5
9
 
6
10
  ## What's E-Invoice
7
11
 
@@ -42,8 +46,25 @@ Einvoice.configure do |config|
42
46
  config.client_secret = ENV['EINVOICE_CLIENT_SECRET']
43
47
  config.encryption_keys = ENV['ENCRYPTION_KEYS']
44
48
  config.format = "json"
49
+ # config.ssl_verify = false # disable TLS certificate verification (default: true)
45
50
  end
46
51
  ```
52
+ ### SSL certificate verification
53
+
54
+ By default, `einvoice` verifies the TLS certificate of the configured
55
+ endpoint (`config.ssl_verify` defaults to `true`). If you need to disable
56
+ this — for example when testing against an endpoint with an incomplete
57
+ certificate chain — you can opt out:
58
+
59
+ ```ruby
60
+ config.ssl_verify = false
61
+ ```
62
+
63
+ Or per-provider:
64
+
65
+ ```ruby
66
+ Einvoice::Tradevan::Provider.new(ssl_verify: false)
67
+ ```
47
68
 
48
69
  ### Initialize
49
70
 
@@ -51,7 +72,7 @@ end
51
72
  client = Einvoice::Client.new(Einvoice::Tradevan::Provider.new)
52
73
  ```
53
74
 
54
- ### Issue an invoice
75
+ ### Issue an invoice (開立電子發票)
55
76
 
56
77
  ```ruby
57
78
  payload = {
@@ -122,7 +143,7 @@ result.data
122
143
  # }
123
144
  ```
124
145
 
125
- ### Cancel an invoice
146
+ ### Cancel an invoice (作廢電子發票)
126
147
 
127
148
  ```ruby
128
149
  payload = {
@@ -149,7 +170,74 @@ result.data
149
170
  # }
150
171
  ```
151
172
 
152
- ### Search invoice by memberId/memberId/sellTargetCode
173
+ ### Issue an allowance (開立折讓單)
174
+
175
+ An allowance against a previously issued invoice is sent through `issue` with `type: "A"`. Each item must reference the original invoice (`invoiceNumber` / `invoiceDate`) and carry its tax-exclusive amount (`itemExclude`).
176
+
177
+ ```ruby
178
+ payload = {
179
+ companyUn: "12345678",
180
+ orgId: "ABCDE",
181
+ type: "A",
182
+ allowanceIdentifier: "12345678_ABCDE_53b2a44e4b3d",
183
+ transactionNumber: "53b2a44e4b3d",
184
+ transactionDate: "20160426",
185
+ paperPrintMode: "0",
186
+ invoiceAlarmMode: "4",
187
+ allowanceExclusiveAmount: "95",
188
+ allowanceTax: "5",
189
+ allowanceInclusiveAmount: "100",
190
+ allowancePaperReturned: "N",
191
+ invoicePaperReturned: "N",
192
+ receiverName: "John Appleseed",
193
+ receiverEmail: "john@gmail.com",
194
+ itemList: [
195
+ {
196
+ saleIdentifier: "12345678_ABCDE_53b2a44e4b3c",
197
+ serialNumber: "0001",
198
+ invoiceNumber: "GX38551078",
199
+ invoiceDate: "20160425",
200
+ productName: "Coffee Latte",
201
+ qty: "1000",
202
+ price: "100",
203
+ itemExclude: "95",
204
+ tax: "5",
205
+ itemTotal: "100",
206
+ taxType: "T"
207
+ }
208
+ ]
209
+ }
210
+ ```
211
+
212
+ ```ruby
213
+ result = client.issue(payload)
214
+ result.successful?
215
+ #=> true
216
+ ```
217
+
218
+ Notes:
219
+
220
+ * `allowanceIdentifier` must be prefixed with `"#{companyUn}_#{orgId}_"`, like `saleIdentifier` on invoices.
221
+ * `allowanceExclusiveAmount` + `allowanceTax` = `allowanceInclusiveAmount`.
222
+
223
+ ### Void an allowance (作廢折讓單)
224
+
225
+ ```ruby
226
+ payload = {
227
+ type: "A",
228
+ companyUn: "12345678",
229
+ allowanceNumber: "ABCDE20160426001",
230
+ allowancePaperReturned: "Y",
231
+ }
232
+ ```
233
+
234
+ ```ruby
235
+ result = client.cancel(payload)
236
+ result.successful?
237
+ #=> true
238
+ ```
239
+
240
+ ### Search invoice by memberId/sellTargetCode (載具/會員發票查詢)
153
241
 
154
242
  ```ruby
155
243
  payload = {
@@ -195,7 +283,7 @@ result.data
195
283
  # ]
196
284
  ```
197
285
 
198
- ### Search invoice detail
286
+ ### Search invoice detail (發票開立明細查詢)
199
287
 
200
288
  ```ruby
201
289
  result = client.search_invoice_detail("GX38551078")
@@ -239,7 +327,7 @@ result.data
239
327
  # }
240
328
  ```
241
329
 
242
- ### Send card info to customer
330
+ ### Send card info to customer (電子發票載具歸戶/中獎通知作業)
243
331
 
244
332
  ```ruby
245
333
  payload = {
@@ -259,7 +347,7 @@ result.successful?
259
347
  result.data
260
348
  ```
261
349
 
262
- ### Get invoice mark info
350
+ ### Get invoice mark info (電子發票字軌號碼配額取號/分配作業)
263
351
 
264
352
  ```ruby
265
353
  payload = {
@@ -291,13 +379,13 @@ result.data
291
379
  # ]
292
380
  ```
293
381
 
294
- ### Get donate unit list
382
+ ### Get donate unit list (受贈機關/社福團體愛心碼清單查詢)
295
383
 
296
384
  ```ruby
297
385
  client.get_donate_unit_list("12345678")
298
386
  ```
299
387
 
300
- ### Get invoice content
388
+ ### Get invoice content (電子發票內容查詢)
301
389
 
302
390
  ```ruby
303
391
  payload = {
data/Rakefile CHANGED
@@ -7,13 +7,51 @@ task default: :spec
7
7
 
8
8
  desc "Download latest e-invoice donation unit list and convert it to JSON"
9
9
  task :update_donation_unit do
10
+ require "csv"
10
11
  require "json"
11
12
  require "faraday"
13
+
12
14
  # From https://data.gov.tw/dataset/31868
13
- data = Faraday.get("https://dataset.einvoice.nat.gov.tw/ods/portal/ODS303W/download/3886F055-EB77-4DF9-98E2-F3F49A7D3434/1/8B227A99-042A-4903-8B34-5715442A227D/0/?fileType=csv").body.force_encoding("utf-8")
14
- orgs = data.split("\n")[1..-1].map do |line|
15
- line.split(",").values_at(2, 1)
16
- end.sort_by(&:first)
15
+ url = "https://dataset.einvoice.nat.gov.tw/ods/portal/ODS303W/download/3886F055-EB77-4DF9-98E2-F3F49A7D3434/1/8B227A99-042A-4903-8B34-5715442A227D/0/?fileType=csv"
16
+ response = Faraday.get(url)
17
+ raise "Download failed: HTTP #{response.status}" unless response.status == 200
18
+
19
+ body = response.body
20
+
21
+ possible = ["UTF-8", "Big5"]
22
+ chosen = nil
23
+ utf8_text = nil
24
+
25
+ possible.each do |code|
26
+ begin
27
+ str = body.dup.force_encoding(code)
28
+ candidate = str.encode("UTF-8")
29
+ if candidate.valid_encoding?
30
+ chosen = code
31
+ utf8_text = candidate
32
+ break
33
+ end
34
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
35
+ next
36
+ end
37
+ end
38
+
39
+ unless chosen
40
+ chosen = "fallback(replace)"
41
+ utf8_text = body.dup.force_encoding("UTF-8").scrub("?")
42
+ puts "::warning::Used fallback encoding with replacement; some characters may be lost"
43
+ end
44
+
45
+ utf8_text = utf8_text.delete_prefix("\uFEFF") # remove possible UTF-8 BOM
46
+ csv = CSV.parse(utf8_text, headers: true)
47
+
48
+ orgs = csv.map do |row|
49
+ code = (row['捐贈碼'] || row[2]).to_s.strip
50
+ name = (row['受捐贈機關或團體名稱'] || row[1]).to_s.strip
51
+ [code, name]
52
+ end.sort_by { |code, _| [code.to_i, code] }
53
+
54
+ File.write "lib/einvoice/donation_unit_list.json", JSON.pretty_generate(orgs.to_h)
17
55
 
18
- File.write "lib/einvoice/donation_unit_list.json", JSON.pretty_generate(Hash[orgs])
19
- end
56
+ puts "Wrote lib/einvoice/donation_unit_list.json (#{orgs.length} entries)"
57
+ end
data/einvoice.gemspec CHANGED
@@ -9,25 +9,30 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["David Yun"]
10
10
  spec.email = ["abookyun@gmail.com"]
11
11
 
12
- spec.summary = %q{A API wrapper for Taiwan e-invoice services.}
12
+ spec.summary = %q{An API wrapper for Taiwan e-invoice services.}
13
13
  spec.homepage = "https://github.com/abookyun/einvoice"
14
14
  spec.license = "MIT"
15
15
 
16
+ spec.metadata = {
17
+ "source_code_uri" => "https://github.com/abookyun/einvoice",
18
+ "changelog_uri" => "https://github.com/abookyun/einvoice/blob/main/CHANGELOG.md",
19
+ "bug_tracker_uri" => "https://github.com/abookyun/einvoice/issues",
20
+ "rubygems_mfa_required" => "true"
21
+ }
22
+
16
23
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
24
  spec.bindir = "exe"
18
25
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
26
  spec.require_paths = ["lib"]
20
27
 
21
- spec.required_ruby_version = '>= 2.0.0'
22
- spec.add_dependency "faraday"
23
- spec.add_dependency "faraday_middleware"
28
+ spec.required_ruby_version = '>= 3.1'
29
+ spec.add_dependency "faraday", "~> 2.1"
24
30
  spec.add_dependency "multi_xml"
25
31
  spec.add_dependency "activemodel"
26
32
  spec.add_dependency "activesupport"
27
- spec.add_dependency "gyoku"
28
33
 
29
34
  spec.add_development_dependency "bundler", "> 1.17"
30
- spec.add_development_dependency "rake", "~> 12.3"
35
+ spec.add_development_dependency "rake", "~> 13.0"
31
36
  spec.add_development_dependency "pry"
32
37
  spec.add_development_dependency "rspec"
33
38
  spec.add_development_dependency "factory_bot"
@@ -7,7 +7,15 @@ module Einvoice
7
7
  end
8
8
 
9
9
  def method_missing(m, *args, &block)
10
- provider.send(m, *args, &block)
10
+ if provider.respond_to?(m)
11
+ provider.public_send(m, *args, &block)
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ def respond_to_missing?(m, include_private = false)
18
+ provider.respond_to?(m) || super
11
19
  end
12
20
  end
13
21
  end
@@ -8,7 +8,8 @@ module Einvoice
8
8
  :client_id,
9
9
  :client_secret,
10
10
  :encryption_keys,
11
- :format
11
+ :format,
12
+ :ssl_verify
12
13
  ].freeze
13
14
 
14
15
  DEFAULT_CLIENT_ID = nil
@@ -17,8 +18,9 @@ module Einvoice
17
18
  DEFAULT_ENDPOINT_URL = nil
18
19
  DEFAULT_FORMAT = ""
19
20
  DEFAULT_ENCRYPTION_KEYS = {}
21
+ DEFAULT_SSL_VERIFY = true
20
22
 
21
- attr_accessor *VALID_OPTIONS_KEYS
23
+ attr_accessor(*VALID_OPTIONS_KEYS)
22
24
 
23
25
  def self.extended(base)
24
26
  base.reset
@@ -41,6 +43,7 @@ module Einvoice
41
43
  self.endpoint_url = DEFAULT_ENDPOINT_URL
42
44
  self.encryption_keys = DEFAULT_ENCRYPTION_KEYS
43
45
  self.format = DEFAULT_FORMAT
46
+ self.ssl_verify = DEFAULT_SSL_VERIFY
44
47
  end
45
48
  end
46
49
  end
@@ -1,5 +1,5 @@
1
- require "faraday_middleware"
2
1
  require "faraday/response/decode_tradevan"
2
+ require "faraday/response/parse_xml"
3
3
 
4
4
  module Einvoice
5
5
  module Connection
@@ -8,7 +8,8 @@ module Einvoice
8
8
  def connection(options = {})
9
9
  connection_options = {
10
10
  headers: { "Accept" => "application/#{format}; charset=utf-8" },
11
- url: endpoint
11
+ url: endpoint,
12
+ ssl: { verify: ssl_verify != false }
12
13
  }.merge(options)
13
14
 
14
15
  ::Faraday::Connection.new(connection_options) do |connection|