workarea-forter 1.2.4 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile +1 -1
- data/app/models/search/admin/order.decorator +1 -1
- data/app/models/workarea/checkout.decorator +24 -0
- data/app/models/workarea/checkout/collect_payment.decorator +7 -43
- data/app/models/workarea/checkout/fraud/forter_analyzer.rb +57 -0
- data/app/models/workarea/order.decorator +0 -7
- data/app/models/workarea/order/fraud_decision.decorator +13 -0
- data/app/models/workarea/order/status/suspected_fraud.rb +3 -9
- data/app/models/workarea/payment.decorator +1 -1
- data/app/models/workarea/payment/status/suspected_fraud.rb +1 -1
- data/app/services/workarea/forter/order.rb +3 -3
- data/app/views/workarea/admin/orders/{forter.html.haml → fraud.html.haml} +4 -7
- data/app/workers/forter/update_status.rb +1 -1
- data/config/initializers/appends.rb +0 -5
- data/config/initializers/workarea.rb +2 -0
- data/config/routes.rb +1 -6
- data/lib/workarea/forter/version.rb +1 -1
- data/test/integration/workarea/admin/forter_order_integration_test.rb +5 -5
- data/test/integration/workarea/storefront/checkouts_integration_test.decorator +69 -0
- data/test/integration/workarea/storefront/forter_integration_test.rb +15 -12
- data/test/lib/workarea/forter/gateway_test.rb +1 -2
- data/test/models/workarea/checkout/forter_collect_payment_test.rb +29 -38
- data/test/models/workarea/checkout_test.decorator +16 -0
- data/test/system/workarea/admin/orders_system_test.decorator +12 -0
- data/test/vcr_cassettes/forter/update_status.yml +31 -26
- data/test/workers/forter/update_status_test.rb +4 -4
- data/workarea-forter.gemspec +1 -1
- metadata +11 -11
- data/app/controllers/workarea/admin/orders_controller.decorator +0 -6
- data/app/models/workarea/forter/decision.rb +0 -18
- data/app/view_models/workarea/admin/order_view_model.decorator +0 -7
- data/app/views/workarea/admin/orders/_forter.html.haml +0 -18
- data/test/system/workarea/admin/forter_system_test.rb +0 -31
- data/test/view_models/workarea/storefront/order_view_model_test.decorator +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c5b05cf277bf2f9053f124dcf7c7cb83096aa2aaf4def84bc5a600f50364ae6e
|
4
|
+
data.tar.gz: 626cdd36a1900d1372572f04be318ae2e57b4be60a8627427d6b0f02a047d97c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 88d605bb65ff10b3764ffd4ab113c066c1534fc968bd3d57fbdd9469d31fcb1f2a92c62fdd0d6f57c86a55c383b2717ce3abb031bd2e4ae309b71ce3c4605ef2
|
7
|
+
data.tar.gz: bd6607c5a93f02a6cd60de6bc36f8000c948c59237e6acfbbf30a739e8f58b05896eeeb5b59be07efc295db3c55a6a0511b582952bfe23b3ec5de4f9832ffb78
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
Workarea Forter 1.3.0 (2019-11-26)
|
2
|
+
--------------------------------------------------------------------------------
|
3
|
+
|
4
|
+
* V3.5 compatability updates
|
5
|
+
|
6
|
+
Update to make the plugin compatible with the 3.5 fraud
|
7
|
+
framework. Moves the decision to post auth.
|
8
|
+
Jeff Yucis
|
9
|
+
|
10
|
+
|
11
|
+
|
1
12
|
Workarea Forter 1.2.4 (2019-10-16)
|
2
13
|
--------------------------------------------------------------------------------
|
3
14
|
|
data/Gemfile
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Checkout, with: :forter do
|
3
|
+
|
4
|
+
# Forter has a post auth model of fraud analysis
|
5
|
+
# This decorator removes the check for fraud.
|
6
|
+
def place_order
|
7
|
+
return false unless complete?
|
8
|
+
return false unless shippable?
|
9
|
+
return false unless payable?
|
10
|
+
|
11
|
+
inventory.purchase
|
12
|
+
return false unless inventory.captured?
|
13
|
+
|
14
|
+
unless payment_collection.purchase
|
15
|
+
inventory.rollback
|
16
|
+
return false
|
17
|
+
end
|
18
|
+
|
19
|
+
result = order.place
|
20
|
+
place_order_side_effects if result
|
21
|
+
result
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -4,23 +4,20 @@ module Workarea
|
|
4
4
|
collect_result = super
|
5
5
|
|
6
6
|
begin
|
7
|
-
|
8
|
-
order_hash = Forter::Order.new(@order).to_h
|
9
|
-
|
10
|
-
response = get_decision(@order_id, order_hash, decision)
|
7
|
+
fraud_analyzer.decide!
|
11
8
|
|
12
9
|
# Rollback all transactions if the original collection
|
13
10
|
# was successful but the forter decision was a decline.
|
14
|
-
if collect_result &&
|
11
|
+
if collect_result && @order.fraud_suspected?
|
15
12
|
payment.rollback!
|
16
|
-
payment.
|
13
|
+
payment.fraud_suspected = true
|
17
14
|
|
18
15
|
payment.save!
|
19
16
|
false
|
20
17
|
else
|
21
18
|
# need to re-add errors indicating the payment operation failed
|
22
19
|
error_messages = payment.errors.messages.clone
|
23
|
-
payment.update_attribute(:
|
20
|
+
payment.update_attribute(:fraud_suspected, false)
|
24
21
|
error_messages.each do |attribute, message|
|
25
22
|
payment.errors.add(attribute, message)
|
26
23
|
end
|
@@ -30,46 +27,13 @@ module Workarea
|
|
30
27
|
rescue => e
|
31
28
|
Forter.log_error(e)
|
32
29
|
return collect_result
|
33
|
-
ensure
|
34
|
-
decision.save!
|
35
30
|
end
|
36
31
|
end
|
37
32
|
|
38
33
|
private
|
39
34
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
# Gets forter decision and builds decision responses on the Forter Decision
|
44
|
-
# model
|
45
|
-
#
|
46
|
-
# @raises Error
|
47
|
-
#
|
48
|
-
# @return [Workarea::Forter::DecisionResponse] response
|
49
|
-
#
|
50
|
-
def get_decision(order_id, order_hash, decision)
|
51
|
-
count = 1
|
52
|
-
begin
|
53
|
-
fraud_response = Forter.gateway.create_decision(@order.id, order_hash)
|
54
|
-
body = JSON.parse(fraud_response.body)
|
55
|
-
response = Forter::DecisionResponse.new(body)
|
56
|
-
|
57
|
-
decision.responses.build(decision_response: response)
|
58
|
-
raise if ERROR_STATUSES.include? fraud_response.status
|
59
|
-
|
60
|
-
response
|
61
|
-
rescue => error
|
62
|
-
decision.responses.build(
|
63
|
-
timed_out: error.is_a?(Faraday::Error::TimeoutError),
|
64
|
-
error: error.message
|
65
|
-
)
|
66
|
-
if count < MAX_RETRIES
|
67
|
-
count += 1
|
68
|
-
retry
|
69
|
-
else
|
70
|
-
raise error
|
71
|
-
end
|
72
|
-
end
|
73
|
-
end
|
35
|
+
def fraud_analyzer
|
36
|
+
@fraud_analyzer ||= Workarea.config.fraud_analyzer.constantize.new(@checkout)
|
37
|
+
end
|
74
38
|
end
|
75
39
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Workarea
|
2
|
+
class Checkout
|
3
|
+
module Fraud
|
4
|
+
class ForterAnalyzer < Analyzer
|
5
|
+
ERROR_STATUSES = 500..599
|
6
|
+
MAX_RETRIES = 2
|
7
|
+
|
8
|
+
def make_decision
|
9
|
+
decision = Workarea::Order::FraudDecision.new
|
10
|
+
order_hash = Forter::Order.new(order).to_h
|
11
|
+
count = 1
|
12
|
+
|
13
|
+
begin
|
14
|
+
fraud_response = Forter.gateway.create_decision(order.id, order_hash)
|
15
|
+
body = JSON.parse(fraud_response.body)
|
16
|
+
|
17
|
+
forter_decision_action = if body["action"] == "decline"
|
18
|
+
:declined
|
19
|
+
else
|
20
|
+
body["action"].optionize.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
raise if ERROR_STATUSES.include? fraud_response.status
|
24
|
+
|
25
|
+
decision.message = body["message"]
|
26
|
+
decision.decision = forter_decision_action
|
27
|
+
decision.response = body
|
28
|
+
|
29
|
+
response = Forter::DecisionResponse.new(body)
|
30
|
+
decision.responses.build(decision_response: response)
|
31
|
+
decision
|
32
|
+
|
33
|
+
rescue => error
|
34
|
+
forter_error_decision.responses.build(
|
35
|
+
timed_out: error.is_a?(Faraday::Error::TimeoutError),
|
36
|
+
error: error.message
|
37
|
+
)
|
38
|
+
forter_error_decision
|
39
|
+
|
40
|
+
if count < MAX_RETRIES
|
41
|
+
count += 1
|
42
|
+
retry
|
43
|
+
else
|
44
|
+
raise error
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def forter_error_decision
|
52
|
+
@forter_error_decision ||= error_decision(nil)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -3,12 +3,5 @@ module Workarea
|
|
3
3
|
decorated do
|
4
4
|
field :forter_tracking_code, type: String
|
5
5
|
end
|
6
|
-
|
7
|
-
def flagged_for_fraud?
|
8
|
-
decision = Workarea::Forter::Decision.find(id) rescue nil
|
9
|
-
return false unless decision.present? && decision.response.present?
|
10
|
-
|
11
|
-
decision.response.action == 'decline'
|
12
|
-
end
|
13
6
|
end
|
14
7
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Order::FraudDecision, with: :forter do
|
3
|
+
decorated do
|
4
|
+
embeds_many :responses, class_name: 'Workarea::Forter::Response'
|
5
|
+
field :external_order_status, type: String, default: "PROCESSING"
|
6
|
+
end
|
7
|
+
|
8
|
+
def response
|
9
|
+
return if responses.empty?
|
10
|
+
responses.sort_by { |r| r.created_at.to_i }.last.decision_response
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -1,13 +1,7 @@
|
|
1
1
|
module Workarea
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
include StatusCalculator::Status
|
6
|
-
|
7
|
-
def in_status?
|
8
|
-
!order.placed? && order.flagged_for_fraud? && !order.canceled?
|
9
|
-
end
|
10
|
-
end
|
2
|
+
decorate Order::Status::SuspectedFraud, with: :forter do
|
3
|
+
def in_status?
|
4
|
+
super && !order.placed? && !order.canceled?
|
11
5
|
end
|
12
6
|
end
|
13
7
|
end
|
@@ -57,8 +57,8 @@ module Workarea
|
|
57
57
|
end
|
58
58
|
|
59
59
|
def delivery_type
|
60
|
-
return "DIGITAL" if order.items.
|
61
|
-
return "HYBRID" if order.items.any?
|
60
|
+
return "DIGITAL" if order.items.none?(&:shipping?)
|
61
|
+
return "HYBRID" if order.items.any?(&:shipping?)
|
62
62
|
"PHYSICAL"
|
63
63
|
end
|
64
64
|
|
@@ -74,7 +74,7 @@ module Workarea
|
|
74
74
|
basicItemData: {
|
75
75
|
name: item.product.name,
|
76
76
|
quantity: item.quantity,
|
77
|
-
type: item.
|
77
|
+
type: item.shipping? ? "TANGIBLE" : "NON_TANGIBLE",
|
78
78
|
price: { amountUSD: item.total_value.to_s },
|
79
79
|
productId: item.product.id
|
80
80
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
- @page_title = t('workarea.admin.orders.
|
1
|
+
- @page_title = t('workarea.admin.orders.fraud.title', id: @order.id)
|
2
2
|
|
3
3
|
.view
|
4
4
|
.view__header
|
@@ -11,7 +11,7 @@
|
|
11
11
|
= render_aux_navigation_for(@order)
|
12
12
|
|
13
13
|
.view__container
|
14
|
-
= render_cards_for(@order, :
|
14
|
+
= render_cards_for(@order, :fraud)
|
15
15
|
|
16
16
|
.view__container.view__container--narrow
|
17
17
|
.grid
|
@@ -21,7 +21,7 @@
|
|
21
21
|
|
22
22
|
%li
|
23
23
|
%strong= t('workarea.admin.orders.forter.status')
|
24
|
-
= @order.
|
24
|
+
= @order.fraud_decision.external_order_status
|
25
25
|
%li
|
26
26
|
%br
|
27
27
|
%strong= link_to t('workarea.admin.orders.forter.console'), "https://portal.forter.com/dashboard/#{@order.id}"
|
@@ -41,7 +41,7 @@
|
|
41
41
|
%th= t('workarea.admin.orders.forter.error')
|
42
42
|
|
43
43
|
%tbody
|
44
|
-
- @order.
|
44
|
+
- @order.fraud_decision.responses.each do |response|
|
45
45
|
%tr
|
46
46
|
%td= response.fraud_decision_action
|
47
47
|
%td= response.decision_response&.body_message
|
@@ -52,6 +52,3 @@
|
|
52
52
|
%li= e["message"]
|
53
53
|
%td= response.error
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
@@ -4,7 +4,7 @@ module Workarea
|
|
4
4
|
include Sidekiq::Worker
|
5
5
|
|
6
6
|
def perform(id, status_hash)
|
7
|
-
decision = Workarea::
|
7
|
+
decision = Workarea::Order.find(id).fraud_decision rescue nil
|
8
8
|
|
9
9
|
if decision.blank?
|
10
10
|
Rails.logger.warn "No decision record found for #{id} during update status"
|
@@ -2,6 +2,8 @@ Workarea.configure do |config|
|
|
2
2
|
config.order_status_calculators.insert(0, 'Workarea::Order::Status::SuspectedFraud')
|
3
3
|
config.payment_status_calculators.insert(0, 'Workarea::Payment::Status::SuspectedFraud')
|
4
4
|
|
5
|
+
config.fraud_analyzer = 'Workarea::Checkout::Fraud::ForterAnalyzer'
|
6
|
+
|
5
7
|
config.forter = ActiveSupport::Configurable::Configuration.new
|
6
8
|
config.forter.site_id = nil
|
7
9
|
|
data/config/routes.rb
CHANGED
@@ -15,15 +15,15 @@ module Workarea
|
|
15
15
|
def test_shows_error_decision
|
16
16
|
order = create_placed_forter_order(email: 'error@workarea.com')
|
17
17
|
|
18
|
-
get admin.
|
19
|
-
message =
|
18
|
+
get admin.fraud_order_path(order)
|
19
|
+
message = order.fraud_decision.response.errors.first["message"]
|
20
20
|
assert(response.body.include?(message))
|
21
21
|
end
|
22
22
|
|
23
|
-
def
|
23
|
+
def test_returns_forter_order_details
|
24
24
|
order = create_placed_forter_order(email: 'approve@workarea.com')
|
25
|
-
decision =
|
26
|
-
get admin.
|
25
|
+
decision = order.fraud_decision
|
26
|
+
get admin.fraud_order_path(order)
|
27
27
|
|
28
28
|
assert(response.body.include?(decision.response.body_message))
|
29
29
|
assert(response.body.include?(decision.response.action))
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Storefront::CheckoutsIntegrationTest, with: :forter do
|
3
|
+
def test_saving_fraud_decision
|
4
|
+
complete_checkout
|
5
|
+
order = Order.desc(:created_at).first
|
6
|
+
|
7
|
+
assert(order.fraud_decision.present?)
|
8
|
+
assert(order.placed?)
|
9
|
+
refute(order.fraud_suspected_at.present?)
|
10
|
+
assert(order.fraud_decided_at.present?)
|
11
|
+
assert_equal(:approve, order.fraud_decision.decision)
|
12
|
+
|
13
|
+
post storefront.cart_items_path,
|
14
|
+
params: {
|
15
|
+
product_id: product.id,
|
16
|
+
sku: product.skus.first,
|
17
|
+
quantity: 1
|
18
|
+
}
|
19
|
+
|
20
|
+
get storefront.checkout_addresses_path
|
21
|
+
patch storefront.checkout_addresses_path,
|
22
|
+
params: {
|
23
|
+
email: 'decline@workarea.com',
|
24
|
+
billing_address: {
|
25
|
+
first_name: 'Ben',
|
26
|
+
last_name: 'Crouse',
|
27
|
+
street: '12 N. 3rd St.',
|
28
|
+
city: 'Philadelphia',
|
29
|
+
region: 'PA',
|
30
|
+
postal_code: '19106',
|
31
|
+
country: 'US',
|
32
|
+
phone_number: '2159251800'
|
33
|
+
},
|
34
|
+
shipping_address: {
|
35
|
+
first_name: 'Ben',
|
36
|
+
last_name: 'Crouse',
|
37
|
+
street: '22 S. 3rd St.',
|
38
|
+
city: 'Philadelphia',
|
39
|
+
region: 'PA',
|
40
|
+
postal_code: '19106',
|
41
|
+
country: 'US',
|
42
|
+
phone_number: '2159251800'
|
43
|
+
}
|
44
|
+
}
|
45
|
+
get storefront.checkout_shipping_path
|
46
|
+
patch storefront.checkout_shipping_path
|
47
|
+
|
48
|
+
get storefront.checkout_payment_path
|
49
|
+
|
50
|
+
patch storefront.checkout_place_order_path,
|
51
|
+
params: {
|
52
|
+
payment: 'new_card',
|
53
|
+
credit_card: {
|
54
|
+
number: '1',
|
55
|
+
month: 1,
|
56
|
+
year: 2020,
|
57
|
+
cvv: '999'
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
order = Order.desc(:created_at).first
|
62
|
+
|
63
|
+
assert_equal(:declined, order.fraud_decision.decision)
|
64
|
+
assert(order.fraud_suspected_at.present?)
|
65
|
+
assert(order.fraud_decided_at.present?)
|
66
|
+
refute(order.placed?)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -9,13 +9,13 @@ module Workarea
|
|
9
9
|
order = Workarea::Order.last
|
10
10
|
payment = Workarea::Payment.last
|
11
11
|
|
12
|
-
|
12
|
+
order.reload
|
13
|
+
decision = order.fraud_decision
|
13
14
|
|
14
15
|
refute(decision.response.suspected_fraud?)
|
15
16
|
|
16
|
-
order.
|
17
|
-
refute(
|
18
|
-
refute(payment.flagged_for_fraud?)
|
17
|
+
refute(order.fraud_suspected?)
|
18
|
+
refute(payment.fraud_suspected?)
|
19
19
|
end
|
20
20
|
|
21
21
|
def test_decision_not_reviewed
|
@@ -24,9 +24,10 @@ module Workarea
|
|
24
24
|
|
25
25
|
order = Workarea::Order.last
|
26
26
|
payment = Workarea::Payment.last
|
27
|
-
decision = Workarea::Forter::Decision.find(order.id)
|
28
27
|
|
29
|
-
|
28
|
+
decision = order.fraud_decision
|
29
|
+
|
30
|
+
refute(payment.fraud_suspected?)
|
30
31
|
refute(decision.response.suspected_fraud?)
|
31
32
|
end
|
32
33
|
|
@@ -35,7 +36,9 @@ module Workarea
|
|
35
36
|
complete_checkout("decline@workarea.com", 'W3blinc1')
|
36
37
|
|
37
38
|
order = Workarea::Order.last
|
38
|
-
|
39
|
+
|
40
|
+
decision = order.fraud_decision
|
41
|
+
|
39
42
|
assert(decision.response.suspected_fraud?)
|
40
43
|
|
41
44
|
payment = Workarea::Payment.last
|
@@ -43,13 +46,12 @@ module Workarea
|
|
43
46
|
|
44
47
|
assert(transaction.cancellation.present?)
|
45
48
|
|
46
|
-
order.reload
|
47
49
|
payment.reload
|
48
50
|
|
49
|
-
assert(order.
|
51
|
+
assert(order.fraud_suspected?)
|
50
52
|
assert_equal(:suspected_fraud, order.status)
|
51
53
|
|
52
|
-
assert(payment.
|
54
|
+
assert(payment.fraud_suspected?)
|
53
55
|
assert_equal(:suspected_fraud, payment.status)
|
54
56
|
end
|
55
57
|
|
@@ -59,9 +61,10 @@ module Workarea
|
|
59
61
|
|
60
62
|
order = Workarea::Order.last
|
61
63
|
payment = Workarea::Payment.last
|
62
|
-
decision = Workarea::Forter::Decision.find(order.id)
|
63
64
|
|
64
|
-
|
65
|
+
decision = order.fraud_decision
|
66
|
+
|
67
|
+
refute(payment.fraud_suspected?)
|
65
68
|
refute(decision.response.suspected_fraud?)
|
66
69
|
assert(decision.response.errors.present?)
|
67
70
|
end
|
@@ -16,7 +16,7 @@ module Workarea
|
|
16
16
|
VCR.use_cassette("forter/get_decision", match_requests_on: [:method, :uri]) do
|
17
17
|
order = create_placed_forter_order(id: "fortertest1234", email: "approve@forter.com")
|
18
18
|
|
19
|
-
forter_decision =
|
19
|
+
forter_decision = order.fraud_decision
|
20
20
|
response = forter_decision.responses.first
|
21
21
|
assert response.decision_response.success?
|
22
22
|
end
|
@@ -31,7 +31,6 @@ module Workarea
|
|
31
31
|
response = gateway.create_decision(order.id, hsh)
|
32
32
|
assert(response.success?)
|
33
33
|
|
34
|
-
|
35
34
|
hsh = {
|
36
35
|
orderId: order.id,
|
37
36
|
eventTime: Time.new.to_i * 1000,
|
@@ -5,56 +5,47 @@ module Workarea
|
|
5
5
|
class ForterCollectPaymentTest < TestCase
|
6
6
|
setup :create_models
|
7
7
|
|
8
|
-
def
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
forter_decision = Forter::Decision.find(@order.id)
|
16
|
-
assert_equal(2, forter_decision.responses.size)
|
17
|
-
forter_decision.responses.each do |response|
|
18
|
-
assert response.timed_out
|
19
|
-
end
|
8
|
+
def test_purchase
|
9
|
+
@collect_payment.purchase
|
10
|
+
@order.reload
|
11
|
+
forter_decision = @order.fraud_decision
|
12
|
+
assert_equal(1, forter_decision.responses.size)
|
13
|
+
assert_equal(:approve, forter_decision.decision)
|
20
14
|
end
|
21
15
|
|
22
|
-
def
|
23
|
-
normal_response = Forter.gateway.create_decision(@order.id, Forter::Order.new(@order).to_h)
|
16
|
+
def test_rescuing_timeout_errors
|
24
17
|
Workarea::Forter::BogusGateway
|
25
18
|
.any_instance
|
26
19
|
.stubs(:create_decision)
|
27
20
|
.raises(Faraday::Error::TimeoutError)
|
28
|
-
.then
|
29
|
-
.returns(normal_response)
|
30
|
-
|
31
21
|
refute(@collect_payment.purchase)
|
32
|
-
forter_decision = Forter::Decision.find(@order.id)
|
33
|
-
assert_equal(2, forter_decision.responses.size)
|
34
22
|
|
35
|
-
|
36
|
-
|
23
|
+
@order.reload
|
24
|
+
forter_decision = @order.fraud_decision
|
25
|
+
assert_equal(:no_decision, forter_decision.decision)
|
26
|
+
|
27
|
+
assert_equal("An error occured during the fraud check: timeout", forter_decision.message)
|
37
28
|
end
|
38
29
|
|
39
30
|
private
|
40
31
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
32
|
+
def create_models
|
33
|
+
@order = Order.create!(email: 'test@workarea.com', total_price: 5.to_m)
|
34
|
+
@checkout = Checkout.new(@order)
|
35
|
+
@payment = Payment.create!(
|
36
|
+
id: @order.id,
|
37
|
+
address: {
|
38
|
+
first_name: "Ben",
|
39
|
+
last_name: "Crouse",
|
40
|
+
street: "22 S 3rd St",
|
41
|
+
city: "Philadelphia",
|
42
|
+
region: "PA",
|
43
|
+
country: Country['US'],
|
44
|
+
postal_code: 19106
|
45
|
+
}
|
46
|
+
)
|
47
|
+
@collect_payment = CollectPayment.new(@checkout)
|
48
|
+
end
|
58
49
|
end
|
59
50
|
end
|
60
51
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate CheckoutTest, with: :forter do
|
3
|
+
def test_place_order_fails_for_fraud
|
4
|
+
@order.email = 'decline@workarea.com'
|
5
|
+
checkout = Checkout.new(@order)
|
6
|
+
|
7
|
+
checkout.expects(:complete?).returns(true)
|
8
|
+
checkout.expects(:shippable?).returns(true)
|
9
|
+
checkout.expects(:payable?).returns(true)
|
10
|
+
checkout.inventory.expects(:purchase).once
|
11
|
+
|
12
|
+
refute(checkout.place_order)
|
13
|
+
refute(@order.reload.placed?)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Admin::OrdersSystemTest, with: :forter do
|
3
|
+
def test_fraud
|
4
|
+
order = create_placed_forter_order(email: 'decline@workarea.com')
|
5
|
+
visit admin.order_path(order)
|
6
|
+
click_link t('workarea.admin.orders.attributes.fraud.title')
|
7
|
+
|
8
|
+
assert(page.has_content?('decline')) # decision
|
9
|
+
assert(page.has_content?(order.fraud_decision.message))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -5,38 +5,41 @@ http_interactions:
|
|
5
5
|
uri: https://api.forter-secure.com/v2/orders/statusfortertest12345
|
6
6
|
body:
|
7
7
|
encoding: UTF-8
|
8
|
-
string: '{"orderId":"statusfortertest12345","orderType":"WEB","timeSentToForter":
|
8
|
+
string: '{"orderId":"statusfortertest12345","orderType":"WEB","timeSentToForter":1574090964000,"checkoutTime":1574090960,"primaryRecipient":{"personalDetails":{"firstName":"Ben","lastName":"Crouse"},"address":{"address1":"22
|
9
9
|
S. 3rd St.","city":"Philadelphia","country":"US","address2":"Second Floor","zip":"19106","region":"PA"},"phone":[{"phone":""}]},"totalAmount":{"amountUSD":"11.00"},"connectionInformation":{"customerIP":"127.0.0.1","userAgent":"Mozilla/5.0
|
10
10
|
(Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0
|
11
|
-
(Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0."},"primaryDeliveryDetails":{"deliveryType":"PHYSICAL","deliveryMethod":"Test
|
11
|
+
(Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0.","forterTokenCookie":"123ABC"},"primaryDeliveryDetails":{"deliveryType":"PHYSICAL","deliveryMethod":"Test
|
12
12
|
0","deliveryPrice":{"amountUSD":"1.00"},"carrier":null},"cartItems":[{"basicItemData":{"name":"Test
|
13
|
-
Product","quantity":2,"type":"TANGIBLE","price":{"amountUSD":"10.00"},"productId":"
|
14
|
-
|
15
|
-
|
13
|
+
Product","quantity":2,"type":"TANGIBLE","price":{"amountUSD":"10.00"},"productId":"BE6FEF44CE"}}],"payment":[{"creditCard":{"bin":"411111","lastFourDigits":"1111","expirationMonth":"01","expirationYear":"2020","cardBrand":"Visa","nameOnCard":"Ben
|
14
|
+
Crouse","paymentGatewayData":{"gatewayName":"not specified","gatewayTransactionId":"53433"},"verificationResults":{"avsFullResult":"","cvvResult":"","authorizationCode":"53433","processorResponseText":"Bogus
|
15
|
+
Gateway: Forced success","processorResponseCode":""}},"billingDetails":{"personalDetails":{"firstName":"Ben","lastName":"Crouse"},"address":{"address1":"12
|
16
|
+
N. 3rd St.","address2":"thrid floor","city":"Philadelphia","country":"US","zip":"19106","region":"PA"},"phone":[{"phone":""}]},"amount":{"amountUSD":"11.00"}}],"accountOwner":{"firstName":"Ben","lastName":"Crouse","email":"approve@forter.com"},"totalDiscount":null}'
|
16
17
|
headers:
|
17
18
|
Content-Type:
|
18
19
|
- application/json
|
20
|
+
X-Forter-Siteid:
|
21
|
+
- 4d12ac5d794c
|
19
22
|
Api-Version:
|
20
|
-
- '2.
|
23
|
+
- '2.3'
|
21
24
|
User-Agent:
|
22
25
|
- Faraday v0.15.4
|
23
26
|
Authorization:
|
24
|
-
- Basic
|
27
|
+
- Basic ODZmMjFiZWU3ZTNiMmU1MWY2YTk5NmIxZTYxNTFmOTA4ZDY1ZWQ0Zjo=
|
25
28
|
Accept-Encoding:
|
26
29
|
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
27
30
|
Accept:
|
28
31
|
- "*/*"
|
29
32
|
response:
|
30
33
|
status:
|
31
|
-
code:
|
32
|
-
message:
|
34
|
+
code: 200
|
35
|
+
message: OK
|
33
36
|
headers:
|
34
37
|
Date:
|
35
|
-
-
|
38
|
+
- Mon, 18 Nov 2019 15:29:28 GMT
|
36
39
|
Content-Type:
|
37
40
|
- application/json; charset=utf-8
|
38
41
|
Content-Length:
|
39
|
-
- '
|
42
|
+
- '186'
|
40
43
|
Connection:
|
41
44
|
- keep-alive
|
42
45
|
Server:
|
@@ -52,27 +55,29 @@ http_interactions:
|
|
52
55
|
Expires:
|
53
56
|
- '0'
|
54
57
|
Etag:
|
55
|
-
- W/"
|
58
|
+
- W/"ba-Ef/isk7mEOWPRXN9i4trCjru7vk"
|
56
59
|
Vary:
|
57
60
|
- Accept-Encoding
|
58
61
|
body:
|
59
62
|
encoding: UTF-8
|
60
|
-
string: '{"status":"
|
63
|
+
string: '{"status":"success","transaction":"statusfortertest12345","action":"approve","message":"
|
64
|
+
| Link in portal: https://portal.forter.com/dashboard/statusfortertest12345","reasonCode":"Test"}'
|
61
65
|
http_version:
|
62
|
-
recorded_at:
|
66
|
+
recorded_at: Mon, 18 Nov 2019 15:29:28 GMT
|
63
67
|
- request:
|
64
68
|
method: post
|
65
69
|
uri: https://api.forter-secure.com/v2/orders/statusfortertest12345
|
66
70
|
body:
|
67
71
|
encoding: UTF-8
|
68
|
-
string: '{"orderId":"statusfortertest12345","orderType":"WEB","timeSentToForter":
|
72
|
+
string: '{"orderId":"statusfortertest12345","orderType":"WEB","timeSentToForter":1574090968000,"checkoutTime":1574090968,"primaryRecipient":{"personalDetails":{"firstName":"Ben","lastName":"Crouse"},"address":{"address1":"22
|
69
73
|
S. 3rd St.","city":"Philadelphia","country":"US","address2":"Second Floor","zip":"19106","region":"PA"},"phone":[{"phone":""}]},"totalAmount":{"amountUSD":"11.00"},"connectionInformation":{"customerIP":"127.0.0.1","userAgent":"Mozilla/5.0
|
70
74
|
(Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0
|
71
|
-
(Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0."},"primaryDeliveryDetails":{"deliveryType":"PHYSICAL","deliveryMethod":"Test
|
75
|
+
(Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0.","forterTokenCookie":"123ABC"},"primaryDeliveryDetails":{"deliveryType":"PHYSICAL","deliveryMethod":"Test
|
72
76
|
0","deliveryPrice":{"amountUSD":"1.00"},"carrier":null},"cartItems":[{"basicItemData":{"name":"Test
|
73
|
-
Product","quantity":2,"type":"TANGIBLE","price":{"amountUSD":"10.00"},"productId":"
|
74
|
-
|
75
|
-
|
77
|
+
Product","quantity":2,"type":"TANGIBLE","price":{"amountUSD":"10.00"},"productId":"BE6FEF44CE"}}],"payment":[{"creditCard":{"bin":"411111","lastFourDigits":"1111","expirationMonth":"01","expirationYear":"2020","cardBrand":"Visa","nameOnCard":"Ben
|
78
|
+
Crouse","paymentGatewayData":{"gatewayName":"not specified","gatewayTransactionId":"53433"},"verificationResults":{"avsFullResult":"","cvvResult":"","authorizationCode":"53433","processorResponseText":"Bogus
|
79
|
+
Gateway: Forced success","processorResponseCode":""}},"billingDetails":{"personalDetails":{"firstName":"Ben","lastName":"Crouse"},"address":{"address1":"12
|
80
|
+
N. 3rd St.","address2":"thrid floor","city":"Philadelphia","country":"US","zip":"19106","region":"PA"},"phone":[{"phone":""}]},"amount":{"amountUSD":"11.00"}}],"accountOwner":{"firstName":"Ben","lastName":"Crouse","email":"approve@forter.com"},"totalDiscount":null}'
|
76
81
|
headers:
|
77
82
|
Content-Type:
|
78
83
|
- application/json
|
@@ -83,7 +88,7 @@ http_interactions:
|
|
83
88
|
User-Agent:
|
84
89
|
- Faraday v0.15.4
|
85
90
|
Authorization:
|
86
|
-
- Basic
|
91
|
+
- Basic ODZmMjFiZWU3ZTNiMmU1MWY2YTk5NmIxZTYxNTFmOTA4ZDY1ZWQ0Zjo=
|
87
92
|
Accept-Encoding:
|
88
93
|
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
89
94
|
Accept:
|
@@ -94,7 +99,7 @@ http_interactions:
|
|
94
99
|
message: OK
|
95
100
|
headers:
|
96
101
|
Date:
|
97
|
-
-
|
102
|
+
- Mon, 18 Nov 2019 15:29:37 GMT
|
98
103
|
Content-Type:
|
99
104
|
- application/json; charset=utf-8
|
100
105
|
Content-Length:
|
@@ -122,13 +127,13 @@ http_interactions:
|
|
122
127
|
string: '{"status":"success","transaction":"statusfortertest12345","action":"approve","message":"
|
123
128
|
| Link in portal: https://portal.forter.com/dashboard/statusfortertest12345","reasonCode":"Test"}'
|
124
129
|
http_version:
|
125
|
-
recorded_at:
|
130
|
+
recorded_at: Mon, 18 Nov 2019 15:29:37 GMT
|
126
131
|
- request:
|
127
132
|
method: put
|
128
133
|
uri: https://api.forter-secure.com/v2/status/statusfortertest12345
|
129
134
|
body:
|
130
135
|
encoding: UTF-8
|
131
|
-
string: '{"orderId":"statusfortertest12345","eventTime":
|
136
|
+
string: '{"orderId":"statusfortertest12345","eventTime":1574090977000,"updatedStatus":"CANCELED_BY_MERCHANT"}'
|
132
137
|
headers:
|
133
138
|
Content-Type:
|
134
139
|
- application/json
|
@@ -139,7 +144,7 @@ http_interactions:
|
|
139
144
|
User-Agent:
|
140
145
|
- Faraday v0.15.4
|
141
146
|
Authorization:
|
142
|
-
- Basic
|
147
|
+
- Basic ODZmMjFiZWU3ZTNiMmU1MWY2YTk5NmIxZTYxNTFmOTA4ZDY1ZWQ0Zjo=
|
143
148
|
Accept-Encoding:
|
144
149
|
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
|
145
150
|
Accept:
|
@@ -150,7 +155,7 @@ http_interactions:
|
|
150
155
|
message: OK
|
151
156
|
headers:
|
152
157
|
Date:
|
153
|
-
-
|
158
|
+
- Mon, 18 Nov 2019 15:29:38 GMT
|
154
159
|
Content-Type:
|
155
160
|
- application/json; charset=utf-8
|
156
161
|
Content-Length:
|
@@ -178,5 +183,5 @@ http_interactions:
|
|
178
183
|
string: '{"status":"success","message":"Transaction #statusfortertest12345 status
|
179
184
|
recieved"}'
|
180
185
|
http_version:
|
181
|
-
recorded_at:
|
186
|
+
recorded_at: Mon, 18 Nov 2019 15:29:38 GMT
|
182
187
|
recorded_with: VCR 2.9.3
|
@@ -3,7 +3,7 @@ require 'test_helper'
|
|
3
3
|
module Workarea
|
4
4
|
class Forter::UpdateStatusTest < TestCase
|
5
5
|
def test_decision_status_update
|
6
|
-
|
6
|
+
order = create_placed_order
|
7
7
|
|
8
8
|
details_hash = {
|
9
9
|
orderId: 1234,
|
@@ -11,11 +11,11 @@ module Workarea
|
|
11
11
|
updatedStatus: "CANCELED_BY_MERCHANT"
|
12
12
|
}
|
13
13
|
|
14
|
-
Workarea::Forter::UpdateStatus.new.perform(
|
14
|
+
Workarea::Forter::UpdateStatus.new.perform(order.id, details_hash)
|
15
15
|
|
16
|
-
|
16
|
+
order.reload
|
17
17
|
|
18
|
-
assert_equal("CANCELED_BY_MERCHANT",
|
18
|
+
assert_equal("CANCELED_BY_MERCHANT", order.fraud_decision.external_order_status)
|
19
19
|
end
|
20
20
|
end
|
21
21
|
end
|
data/workarea-forter.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: workarea-forter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeff Yucis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-11-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: workarea
|
@@ -19,7 +19,7 @@ dependencies:
|
|
19
19
|
version: 3.x
|
20
20
|
- - ">="
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '3.
|
22
|
+
version: '3.5'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -29,7 +29,7 @@ dependencies:
|
|
29
29
|
version: 3.x
|
30
30
|
- - ">="
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '3.
|
32
|
+
version: '3.5'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: faraday
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -67,14 +67,15 @@ files:
|
|
67
67
|
- LICENSE
|
68
68
|
- README.md
|
69
69
|
- Rakefile
|
70
|
-
- app/controllers/workarea/admin/orders_controller.decorator
|
71
70
|
- app/controllers/workarea/storefront/application_controller.decorator
|
72
71
|
- app/models/search/admin/order.decorator
|
72
|
+
- app/models/workarea/checkout.decorator
|
73
73
|
- app/models/workarea/checkout/collect_payment.decorator
|
74
|
-
- app/models/workarea/
|
74
|
+
- app/models/workarea/checkout/fraud/forter_analyzer.rb
|
75
75
|
- app/models/workarea/forter/response.rb
|
76
76
|
- app/models/workarea/fulfillment.decorator
|
77
77
|
- app/models/workarea/order.decorator
|
78
|
+
- app/models/workarea/order/fraud_decision.decorator
|
78
79
|
- app/models/workarea/order/status/suspected_fraud.rb
|
79
80
|
- app/models/workarea/payment.decorator
|
80
81
|
- app/models/workarea/payment/saved_credit_card.decorator
|
@@ -88,9 +89,7 @@ files:
|
|
88
89
|
- app/services/workarea/forter/tender/gift_card.rb
|
89
90
|
- app/services/workarea/forter/tender/paypal.rb
|
90
91
|
- app/services/workarea/forter/tender/store_credit.rb
|
91
|
-
- app/
|
92
|
-
- app/views/workarea/admin/orders/_forter.html.haml
|
93
|
-
- app/views/workarea/admin/orders/forter.html.haml
|
92
|
+
- app/views/workarea/admin/orders/fraud.html.haml
|
94
93
|
- app/views/workarea/storefront/_forter_tracking.html.haml
|
95
94
|
- app/workers/forter/update_status.rb
|
96
95
|
- app/workers/workarea/save_user_order_details.decorator
|
@@ -144,15 +143,17 @@ files:
|
|
144
143
|
- test/integration/workarea/forter_cybersource_response_code_integration_test.rb
|
145
144
|
- test/integration/workarea/forter_moneris_response_code_integration_test.rb
|
146
145
|
- test/integration/workarea/forter_payflow_pro_response_code_test.rb
|
146
|
+
- test/integration/workarea/storefront/checkouts_integration_test.decorator
|
147
147
|
- test/integration/workarea/storefront/forter_integration_test.rb
|
148
148
|
- test/lib/workarea/forter/gateway_test.rb
|
149
149
|
- test/models/workarea/checkout/forter_collect_payment_test.rb
|
150
|
+
- test/models/workarea/checkout_test.decorator
|
150
151
|
- test/models/workarea/forter_payment_test.rb
|
151
152
|
- test/models/workarea/payment/forter_credit_card_test.rb
|
152
153
|
- test/models/workarea/payment/forter_saved_credit_card_test.rb
|
153
154
|
- test/services/workarea/forter/order_test.rb
|
154
155
|
- test/support/workarea/forter_api_config.rb
|
155
|
-
- test/system/workarea/admin/
|
156
|
+
- test/system/workarea/admin/orders_system_test.decorator
|
156
157
|
- test/system/workarea/storefront/forter_braintree_checkout_system_test.rb
|
157
158
|
- test/system/workarea/storefront/forter_tracking_system_test.rb
|
158
159
|
- test/system/workarea/storefront/guest_checkout_system_test.decorator
|
@@ -169,7 +170,6 @@ files:
|
|
169
170
|
- test/vcr_cassettes/forter/system/braintree_declined.yml
|
170
171
|
- test/vcr_cassettes/forter/system/braintree_not_reviewed.yml
|
171
172
|
- test/vcr_cassettes/forter/update_status.yml
|
172
|
-
- test/view_models/workarea/storefront/order_view_model_test.decorator
|
173
173
|
- test/workers/forter/update_status_test.rb
|
174
174
|
- workarea-forter.gemspec
|
175
175
|
homepage: https://github.com/workarea-commerce/workarea-forter
|
@@ -1,18 +0,0 @@
|
|
1
|
-
module Workarea
|
2
|
-
module Forter
|
3
|
-
class Decision
|
4
|
-
include ApplicationDocument
|
5
|
-
|
6
|
-
embeds_many :responses, class_name: 'Workarea::Forter::Response'
|
7
|
-
field :external_order_status, type: String, default: "PROCESSING"
|
8
|
-
|
9
|
-
index(created_at: -1)
|
10
|
-
|
11
|
-
# the last response returned from the forter service.
|
12
|
-
def response
|
13
|
-
return if responses.empty?
|
14
|
-
responses.sort_by { |r| r.created_at.to_i }.last.decision_response
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
@@ -1,18 +0,0 @@
|
|
1
|
-
- if order.decision.present?
|
2
|
-
.grid__cell
|
3
|
-
.card{ class: card_classes(:forter, local_assigns[:active]) }
|
4
|
-
= link_to forter_order_path(order), class: 'card__header' do
|
5
|
-
%span.card__header-text= t('workarea.admin.orders.cards.forter.title')
|
6
|
-
= inline_svg 'workarea/admin/icons/link.svg', class: 'card__icon'
|
7
|
-
|
8
|
-
- if local_assigns[:active].blank?
|
9
|
-
.card__body
|
10
|
-
%ul.list-reset
|
11
|
-
%li
|
12
|
-
%strong= t('workarea.admin.orders.cards.forter.decision')
|
13
|
-
= order.decision.response&.action
|
14
|
-
|
15
|
-
- if order.decision.response&.reason_code.present?
|
16
|
-
%li
|
17
|
-
%strong= t('workarea.admin.orders.cards.forter.reason_code')
|
18
|
-
= order.decision.response.reason_code
|
@@ -1,31 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
|
3
|
-
module Workarea
|
4
|
-
module Adming
|
5
|
-
class ForterSystemTest < Workarea::SystemTest
|
6
|
-
include Admin::IntegrationTest
|
7
|
-
|
8
|
-
def test_viewing_forter_order_admin
|
9
|
-
checkout = create_purchasable_checkout
|
10
|
-
order = checkout.order
|
11
|
-
|
12
|
-
normal_response = Forter.gateway.create_decision(
|
13
|
-
order.id,
|
14
|
-
Forter::Order.new(order).to_h
|
15
|
-
)
|
16
|
-
Workarea::Forter::BogusGateway
|
17
|
-
.any_instance
|
18
|
-
.stubs(:create_decision)
|
19
|
-
.raises(Faraday::Error::TimeoutError)
|
20
|
-
.then
|
21
|
-
.returns(normal_response)
|
22
|
-
|
23
|
-
assert(checkout.place_order)
|
24
|
-
|
25
|
-
visit admin.forter_order_path order
|
26
|
-
|
27
|
-
assert page.has_content?("timeout")
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|