ach_client 1.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ebc1c8a970fae00ba2d286d1a44d9b21dccfe153943112cf5aa8c2b55684944
4
- data.tar.gz: '0479726dff32adff969b8e5411aa4df9752d122ebc29dea6942bdf2afa894114'
3
+ metadata.gz: 8e40c40c7ee42a6264c64f512772b254fac48b70cb7bb323ba238594c3d2d50c
4
+ data.tar.gz: a6d925e6f0a9682e085de0f1d12240c827cf769d521ee66fabe7001ade43fd64
5
5
  SHA512:
6
- metadata.gz: 6cac06e017d5919800b050cbd0f839e1b288df41391fe4876766f7c26c2d4bd5910755abfd41c44c17b41c5114e29b31ec6944fc8e197a36b6ce5ebba3ee5226
7
- data.tar.gz: 440a98bc9db3ee2cc0158ecd05665e6c80105d6fd35d48becfe41de1cd69f97f9675d25449850294b8e040d5059f05e7567ff0d14914d5d0bc55c2a9817ecde2
6
+ metadata.gz: 86303a731c6e1c4de5f4a82fa8a12f2f9f955e3ef70601c423d907282b50e2e5992a762f4736223bab87672b28a86b566fa112c55c0e9a0f7fd2c9c819dbc6c3
7
+ data.tar.gz: 71299fac7355cd6b0a48e0245e8aa19e01f809f4acdcc00f97afda028c0f18dc0bb8792fabd5b242c6e590317f83385cef7e434fac395d3ad3ac8371897a1cc4
@@ -1,10 +1,15 @@
1
1
  language: ruby
2
- script:
3
- - bundle exec rake test
4
- - bundle exec codeclimate-test-reporter
5
2
  before_install:
6
- - export TZ=US/Eastern
7
- - export CODECLIMATE_REPO_TOKEN=1fc324a28beed303e632765cca14487904a23023d85b9127a288f04cc007d28d
8
- - date
3
+ - export TZ=US/Eastern
4
+ - export CODECLIMATE_REPO_TOKEN=1fc324a28beed303e632765cca14487904a23023d85b9127a288f04cc007d28d
5
+ - export CC_TEST_REPORTER_ID=1fc324a28beed303e632765cca14487904a23023d85b9127a288f04cc007d28d
6
+ - date
7
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
8
+ - chmod +x ./cc-test-reporter
9
+ - ./cc-test-reporter before-build
10
+ script:
11
+ - bundle exec rake test
12
+ after_script:
13
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
9
14
  notifications:
10
15
  email: false
@@ -0,0 +1,32 @@
1
+ ### 2.1.0
2
+
3
+ * Add AchClient::Fake::AchStatusChecker to facilitate response poller testing
4
+
5
+ ### 2.0.0
6
+
7
+ * Add new AchClient::ICheckGateway::InstantRejectionError to handle API errors raised by ICheckGateway in situations
8
+ where other providers would accept the transaction and issue a return when polling in the future. This is a breaking
9
+ change as previous versions would raise a RuntimeError with the message 'ICheckGateway ACH Transaction Failure' and
10
+ including the API response.
11
+
12
+ * Include "internal corrections" (return codes starting with 'XZ') in the AchClient::ReturnCode#correction? predicate
13
+
14
+ ### 1.1.0
15
+
16
+ * Add AchClient::Fake provider to facilitate testing
17
+
18
+ ### 1.0.3
19
+
20
+ * Add presumed description for X09 return code
21
+
22
+ ### 1.0.2
23
+
24
+ * Add previously undocumented X01 internal return code
25
+
26
+ ### 1.0.1
27
+
28
+ * Remove newline characters from fields before generating NACHA files
29
+
30
+ ### 1.0.0
31
+
32
+ * Prior to 1.0.0, ach_client did not have a stable API.
data/README.md CHANGED
@@ -115,6 +115,16 @@ returned by the `#send` method. So you should:
115
115
 
116
116
  4) When you eventually poll the provider for the status of your transactions, you can use the `external_ach_id` you stored to reconcile the returned data against your records
117
117
 
118
+ ### ICheckGateway instant rejection caveat
119
+
120
+ ICheckGateway sometimes returns an API error when a valid ACH transaction is sent in a handful of
121
+ rejection scenarios. This is unusual because most providers will accept the transaction, return an
122
+ `external_ach_id`, and then supply the rejection info when you poll for responses (see section on response polling
123
+ below) at a later date. This idiosyncrasy is handled by raising an exception `InstantRejectionError` which contains
124
+ information about the premature ACH return. In this case, no `external_ach_id` is returned by the `#send` method
125
+ because ICheck does not return one, nor do they maintain a record of the transaction in their system. See the
126
+ `InstantRejectionError` class for more details.
127
+
118
128
  ## Batched ACH transactions
119
129
 
120
130
  A group of ACH transactions can also be sent in a single batched transaction to
@@ -145,6 +155,12 @@ SFTP+NACHA providers take an optional `batch_number` parameter which may be used
145
155
  )
146
156
  ```
147
157
 
158
+ ## Testing
159
+
160
+ A fake ACH provider (in the `AchClient::Fake` namespace) is included to facilitate testing on staging environments.
161
+ This provider behaves the same way as any other without actually sending any transactions. The transaction sending
162
+ methods return the given `external_ach_id`s with no side-effects.
163
+
148
164
  ## Response Polling - Checking Transaction Status
149
165
 
150
166
  None of the providers support querying for transaction status by external_ach_id. Instead, we must query by date and
@@ -181,6 +197,12 @@ information has changed. Check the return code on the response object for
181
197
  details on what happened. Check the corrections hash on the response object for
182
198
  the new attributes
183
199
 
200
+ ### Testing
201
+
202
+ The fake ACH provider namespace provides a status checker `AchClient::Fake::AchStatusChecker` that always returns the
203
+ same responses. The external_ach_id for each response will be one of
204
+ `['processing', 'settled', 'returned', 'corrected', 'late_returned']`
205
+
184
206
  ## Logging
185
207
 
186
208
  For record keeping purposes, there is a log provider that allows you to hook
@@ -43,13 +43,12 @@ Gem::Specification.new do |spec|
43
43
  # Asynchronocity w/out extra infrastucture dependency (database/redis)
44
44
  spec.add_dependency 'sucker_punch', '~> 2'
45
45
 
46
- spec.add_development_dependency 'bundler', '~> 1.12'
47
46
  spec.add_development_dependency 'codeclimate-test-reporter'
48
47
  spec.add_development_dependency 'minitest-reporters'
49
48
  spec.add_development_dependency 'minitest', '~> 5'
50
- spec.add_development_dependency 'mocha'
49
+ spec.add_development_dependency 'mocha', '~> 1'
51
50
  spec.add_development_dependency 'pry'
52
- spec.add_development_dependency 'rake', '~> 10'
51
+ spec.add_development_dependency 'rake', '>= 12.3.3'
53
52
  spec.add_development_dependency 'simplecov'
54
53
  spec.add_development_dependency 'timecop'
55
54
  spec.add_development_dependency 'vcr'
@@ -343,6 +343,10 @@
343
343
  code: 'X00'
344
344
  description: "Ok for submission"
345
345
  reason: "Ok for submission"
346
+ -
347
+ code: 'X01'
348
+ description: "The available and/or cash reserve balance is not sufficient to cover the dollar value of the debit Entry."
349
+ reason: "Insufficient Funds"
346
350
  -
347
351
  code: 'X02'
348
352
  description: "A previously active account has been closed by action of the customer or the RDFI."
@@ -367,6 +371,10 @@
367
371
  code: 'X08'
368
372
  description: "The Receiver has placed a stop payment order on this debit Entry."
369
373
  reason: "Payment Stopped"
374
+ -
375
+ code: 'X09'
376
+ description: "A sufficient ledger balance exists to satisfy the dollar value of the transaction, but the available balance is below the dollar value of the debit Entry."
377
+ reason: "Uncollected funds due to uncleared deposit"
370
378
  -
371
379
  code: 'X10'
372
380
  description: "The RDFI has been notified by the Receiver that the Entry is unauthorized, improper, or ineligible."
@@ -9,6 +9,9 @@ module AchClient
9
9
  # The first character in an internal return code
10
10
  INTERNAL_START_CHARACTER = 'X'
11
11
 
12
+ # Returns that are both internal and corrections start with this string
13
+ INTERNAL_CORRECTION_STRING = 'XZ'
14
+
12
15
  attr_accessor :code,
13
16
  :description,
14
17
  :reason
@@ -25,7 +28,7 @@ module AchClient
25
28
 
26
29
  # @return Whether or not this return is a correction/notice of change
27
30
  def correction?
28
- @code.start_with?(CORRECTION_START_CHARACTER)
31
+ @code.start_with?(CORRECTION_START_CHARACTER) || @code.start_with?(INTERNAL_CORRECTION_STRING)
29
32
  end
30
33
 
31
34
  # @return Whether or not the return is internal
@@ -0,0 +1,11 @@
1
+ module AchClient
2
+ class Fake
3
+ class AchBatch < Abstract::AchBatch
4
+
5
+ # Fake batch ACH sending just returns the provided external_ach_ids to indicate success
6
+ def do_send_batch
7
+ @ach_transactions.map(&:external_ach_id)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,42 @@
1
+ module AchClient
2
+ class Fake
3
+ # Fake ACH polling that always returns the same set of results.
4
+ class AchStatusChecker < Abstract::AchStatusChecker
5
+
6
+ def self.most_recent
7
+ in_range(start_date: Date.today - 3.days, end_date: Date.today)
8
+ end
9
+
10
+ def self.in_range(start_date:, end_date:)
11
+ {
12
+ 'processing' => [AchClient::ProcessingAchResponse.new(amount: 100.0, date: start_date)],
13
+ 'settled' => [AchClient::SettledAchResponse.new(amount: 100.0, date: start_date)],
14
+ 'returned' => [
15
+ AchClient::ReturnedAchResponse.new(
16
+ amount: 100.0,
17
+ date: start_date,
18
+ return_code: AchClient::ReturnCodes.find_by(code: 'R01')
19
+ )
20
+ ],
21
+ 'corrected' => [
22
+ AchClient::CorrectedAchResponse.new(
23
+ amount: 100.0,
24
+ date: start_date,
25
+ return_code: AchClient::ReturnCodes.find_by(code: 'XZ2'),
26
+ corrections: '123456789'
27
+ )
28
+ ],
29
+ 'late_returned' => [
30
+ AchClient::SettledAchResponse.new(amount: 100.0, date: start_date),
31
+ AchClient::ReturnedAchResponse.new(
32
+ amount: 100.0,
33
+ date: end_date,
34
+ return_code: AchClient::ReturnCodes.find_by(code: 'R08')
35
+ )
36
+ ]
37
+ }
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,11 @@
1
+ module AchClient
2
+ class Fake
3
+ class AchTransaction < Abstract::AchTransaction
4
+
5
+ # Fake ACH sending just returns the provided external_ach_id to indicate success
6
+ def do_send
7
+ external_ach_id
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ module AchClient
2
+ class Fake
3
+ end
4
+ end
@@ -3,11 +3,23 @@ module AchClient
3
3
  # ICheckGateway implementation for AchTransaction
4
4
  class AchTransaction < Abstract::AchTransaction
5
5
 
6
+ # When ICheck API gives us an error response containing a correction, the response field looks like this:
7
+ # DECLINED - Notice of Change (XXX - Change Data: YYYYYY)
8
+ # Where X is a three character string representing the return code (example: C01)
9
+ # Where Y is any number of digits representing the updated information for the correction - the
10
+ # ACH attribute that should be updated (example: 123456789)
11
+ # The (\w{3}) capture group matches the return code, while the (\d+) capture group matches the correction data
12
+ # for later use.
13
+ NOC_RESPONSE_MATCHER = /DECLINED - Notice of Change \((\w{3}) - Change Data: (\d+)\)/
14
+
6
15
  # Sends this transaction to ICheckGateway
7
16
  # If successful, returns a string from the response that seems to be
8
17
  # a unique identifier for the transaction from ICheckGateway
9
18
  # Raises an exception with as much info as possible if something goes
10
- # wrong
19
+ # wrong.
20
+ # ICheck sometimes returns an API error for certain rejection scenarios. In this case we raise a
21
+ # InstantRejectionError which can be caught to handle any business logic appropriate for this edge case.
22
+ # The exception contains a method ach_response that returns the information about the return.
11
23
  # @return [String] a string returned by ICheckGateway - external_ach_id
12
24
  def do_send
13
25
  # The response comes back as a | separated list of field values with
@@ -21,6 +33,19 @@ module AchClient
21
33
  if response[0] == 'APPROVED'
22
34
  # Return the confirmation number
23
35
  response[7]
36
+ elsif response[0].include?('DECLINED - Notice of Change')
37
+ return_code, addendum = NOC_RESPONSE_MATCHER.match(response[0]).captures
38
+ # The API error message incorrectly uses the normal correction return codes when the internal correction
39
+ # return codes should be used instead (since the transaction is never forwarded through to the NACHA system)
40
+ # Correcting this involves replacing `C0` with `XZ` (ie C01 becomes XZ1)
41
+ corrected_return_code = "XZ#{return_code.last}"
42
+ raise ICheckGateway::InstantRejectionError.new(
43
+ nacha_return_code: corrected_return_code,
44
+ addendum: addendum,
45
+ transaction: self
46
+ ), response[0]
47
+ elsif response[0].include?('DECLINED - Invalid Routing Number')
48
+ raise ICheckGateway::InstantRejectionError.new(nacha_return_code: 'X13', transaction: self), response[0]
24
49
  else
25
50
  # Don't have a reliable way of getting the error message, so we will
26
51
  # just raise the whole response.
@@ -0,0 +1,33 @@
1
+ module AchClient
2
+ class ICheckGateway
3
+ # ICheckGateway sometimes returns an API error when a valid ACH transaction is sent in a handful of
4
+ # rejection scenarios. This is unusual because most providers will accept the transaction, return an
5
+ # external_ach_id, and then supply the rejection info when you poll for responses at a later date.
6
+ # So far we have observed this happening in the following scenarios:
7
+ # - When an invalid routing number is supplied (X13 - Invalid ACH Routing Number - Entry contains a Receiving DFI
8
+ # Identification or Gateway Identification that is not a valid ACH routing number.)
9
+ # - When there is a Notice of Change for the account number (C01 - ACH Change Code. Incorrect Account Number)
10
+ # - When there is a Notice of Change for the routing number (C02 - ACH Change Code. Incorrect Transit Route)
11
+ # This exception can be caught to handle the API error in the appropriate manner.
12
+ # The NACHA return code inferred from the error message is retrievable from the exception instance as well as any
13
+ # addendum information provided by the API error (ie the correct new account/routing number)
14
+ class InstantRejectionError < RuntimeError
15
+ attr_reader :ach_response
16
+
17
+ def initialize(message = nil, nacha_return_code:, addendum: nil, transaction:)
18
+ super(message)
19
+ return_code = ReturnCodes.find_by(code: nacha_return_code)
20
+ response_args = {
21
+ amount: transaction.amount,
22
+ date: transaction.effective_entry_date,
23
+ return_code: return_code,
24
+ }
25
+ @ach_response = if return_code.correction?
26
+ CorrectedAchResponse.new(**response_args, corrections: addendum)
27
+ else
28
+ ReturnedAchResponse.new(**response_args)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,4 +1,4 @@
1
1
  module AchClient
2
2
  # Increment this when changes are published
3
- VERSION = '1.0.1'
3
+ VERSION = '2.1.0'
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ach_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Cotter
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-04-02 00:00:00.000000000 Z
11
+ date: 2020-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ach
@@ -80,20 +80,6 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '2'
83
- - !ruby/object:Gem::Dependency
84
- name: bundler
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '1.12'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '1.12'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: codeclimate-test-reporter
99
85
  requirement: !ruby/object:Gem::Requirement
@@ -140,16 +126,16 @@ dependencies:
140
126
  name: mocha
141
127
  requirement: !ruby/object:Gem::Requirement
142
128
  requirements:
143
- - - ">="
129
+ - - "~>"
144
130
  - !ruby/object:Gem::Version
145
- version: '0'
131
+ version: '1'
146
132
  type: :development
147
133
  prerelease: false
148
134
  version_requirements: !ruby/object:Gem::Requirement
149
135
  requirements:
150
- - - ">="
136
+ - - "~>"
151
137
  - !ruby/object:Gem::Version
152
- version: '0'
138
+ version: '1'
153
139
  - !ruby/object:Gem::Dependency
154
140
  name: pry
155
141
  requirement: !ruby/object:Gem::Requirement
@@ -168,16 +154,16 @@ dependencies:
168
154
  name: rake
169
155
  requirement: !ruby/object:Gem::Requirement
170
156
  requirements:
171
- - - "~>"
157
+ - - ">="
172
158
  - !ruby/object:Gem::Version
173
- version: '10'
159
+ version: 12.3.3
174
160
  type: :development
175
161
  prerelease: false
176
162
  version_requirements: !ruby/object:Gem::Requirement
177
163
  requirements:
178
- - - "~>"
164
+ - - ">="
179
165
  - !ruby/object:Gem::Version
180
- version: '10'
166
+ version: 12.3.3
181
167
  - !ruby/object:Gem::Dependency
182
168
  name: simplecov
183
169
  requirement: !ruby/object:Gem::Requirement
@@ -276,6 +262,7 @@ files:
276
262
  - ".tool-versions"
277
263
  - ".tool-versions-e"
278
264
  - ".travis.yml"
265
+ - CHANGELOG.md
279
266
  - Gemfile
280
267
  - README.md
281
268
  - Rakefile
@@ -326,12 +313,17 @@ files:
326
313
  - lib/ach_client/providers/soap/ach_works/date_formatter.rb
327
314
  - lib/ach_client/providers/soap/ach_works/response_record_processor.rb
328
315
  - lib/ach_client/providers/soap/ach_works/transaction_type_transformer.rb
316
+ - lib/ach_client/providers/soap/fake/ach_batch.rb
317
+ - lib/ach_client/providers/soap/fake/ach_status_checker.rb
318
+ - lib/ach_client/providers/soap/fake/ach_transaction.rb
319
+ - lib/ach_client/providers/soap/fake/fake.rb
329
320
  - lib/ach_client/providers/soap/i_check_gateway/account_type_transformer.rb
330
321
  - lib/ach_client/providers/soap/i_check_gateway/ach_batch.rb
331
322
  - lib/ach_client/providers/soap/i_check_gateway/ach_status_checker.rb
332
323
  - lib/ach_client/providers/soap/i_check_gateway/ach_transaction.rb
333
324
  - lib/ach_client/providers/soap/i_check_gateway/company_info.rb
334
325
  - lib/ach_client/providers/soap/i_check_gateway/i_check_gateway.rb
326
+ - lib/ach_client/providers/soap/i_check_gateway/instant_rejection_error.rb
335
327
  - lib/ach_client/providers/soap/i_check_gateway/response_record_processor.rb
336
328
  - lib/ach_client/providers/soap/i_check_gateway/transaction_type_transformer.rb
337
329
  - lib/ach_client/providers/soap/soap_provider.rb