workarea-cyber_source 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +20 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- data/.github/ISSUE_TEMPLATE/documentation-request.md +17 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.gitignore +14 -0
- data/.rails-rubocop.yml +140 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +39 -0
- data/CODE_OF_CONDUCT.md +3 -0
- data/CONTRIBUTING.md +3 -0
- data/Gemfile +11 -0
- data/LICENSE +52 -0
- data/README.md +27 -0
- data/Rakefile +59 -0
- data/app/models/workarea/payment/authorize/credit_card.decorator +45 -0
- data/app/models/workarea/payment/capture/credit_card.decorator +14 -0
- data/app/models/workarea/payment/purchase/credit_card.decorator +48 -0
- data/app/models/workarea/payment/refund/credit_card.decorator +14 -0
- data/app/models/workarea/payment/store_credit_card.decorator +13 -0
- data/bin/rails +20 -0
- data/config/initializers/gateway.rb +1 -0
- data/lib/active_merchant/billing/bogus_cyber_source_gateway.rb +97 -0
- data/lib/active_merchant/billing/cyber_source_fix.rb +34 -0
- data/lib/workarea/cyber_source.rb +36 -0
- data/lib/workarea/cyber_source/engine.rb +8 -0
- data/lib/workarea/cyber_source/version.rb +5 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/bin/setup +38 -0
- data/test/dummy/bin/update +29 -0
- data/test/dummy/bin/yarn +11 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/config/application.rb +28 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/cable.yml +10 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +56 -0
- data/test/dummy/config/environments/production.rb +91 -0
- data/test/dummy/config/environments/test.rb +44 -0
- data/test/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/test/dummy/config/initializers/assets.rb +14 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +4 -0
- data/test/dummy/config/initializers/workarea.rb +5 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +33 -0
- data/test/dummy/config/puma.rb +56 -0
- data/test/dummy/config/routes.rb +5 -0
- data/test/dummy/config/secrets.yml +32 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/db/seeds.rb +2 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/package.json +5 -0
- data/test/integration/workarea/cyber_source_integration_test.rb +191 -0
- data/test/models/workarea/payment/authorize/credit_card_test.decorator +68 -0
- data/test/models/workarea/payment/capture/credit_card_test.decorator +17 -0
- data/test/models/workarea/payment/purchase/credit_card_test.decorator +60 -0
- data/test/models/workarea/payment/refund/credit_card_test.decorator +17 -0
- data/test/models/workarea/payment/store_credit_card_test.decorator +15 -0
- data/test/support/workarea/cyber_source_support_vcr_config.rb +23 -0
- data/test/support/workarea/workarea_3_2_backports.rb +57 -0
- data/test/teaspoon_env.rb +6 -0
- data/test/test_helper.rb +11 -0
- data/test/vcr_cassettes/cyber_source/auth_capture.yml +254 -0
- data/test/vcr_cassettes/cyber_source/auth_capture_refund.yml +323 -0
- data/test/vcr_cassettes/cyber_source/auth_void.yml +250 -0
- data/test/vcr_cassettes/cyber_source/purchase_refund.yml +251 -0
- data/test/vcr_cassettes/cyber_source/purchase_void.yml +247 -0
- data/test/vcr_cassettes/cyber_source/store_auth.yml +179 -0
- data/test/vcr_cassettes/cyber_source/store_purchase.yml +180 -0
- data/workarea-cyber_source.gemspec +19 -0
- metadata +133 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Payment::Authorize::CreditCard, with: :cyber_source do
|
3
|
+
decorated { delegate :address, to: :tender }
|
4
|
+
|
5
|
+
def initialize(tender, transaction, options = {})
|
6
|
+
super
|
7
|
+
@options = @options.merge(
|
8
|
+
email: email,
|
9
|
+
billing_address: billing_address,
|
10
|
+
order_id: order_id
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def complete!
|
15
|
+
return unless Payment::StoreCreditCard.new(tender, options).save!
|
16
|
+
|
17
|
+
transaction.response = handle_active_merchant_errors do
|
18
|
+
gateway.authorize(transaction.amount.cents, tender.token, options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def order_id
|
25
|
+
tender.payment.id
|
26
|
+
end
|
27
|
+
|
28
|
+
def email
|
29
|
+
return unless tender.profile.present?
|
30
|
+
|
31
|
+
tender.profile.email
|
32
|
+
end
|
33
|
+
|
34
|
+
def billing_address
|
35
|
+
{
|
36
|
+
address1: address.street,
|
37
|
+
address2: address.street_2,
|
38
|
+
city: address.city,
|
39
|
+
state: address.region,
|
40
|
+
country: address.country.try(:alpha2),
|
41
|
+
zip: address.postal_code
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Payment::Capture::CreditCard, with: :cyber_source do
|
3
|
+
def complete!
|
4
|
+
validate_reference!
|
5
|
+
|
6
|
+
transaction.response = handle_active_merchant_errors do
|
7
|
+
gateway.capture(
|
8
|
+
transaction.amount.cents,
|
9
|
+
transaction.reference.response.authorization
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Payment::Purchase::CreditCard, with: :cyber_source do
|
3
|
+
decorated { delegate :address, to: :tender }
|
4
|
+
|
5
|
+
def initialize(tender, transaction, options = {})
|
6
|
+
super
|
7
|
+
@options = @options.merge(
|
8
|
+
email: email,
|
9
|
+
billing_address: billing_address,
|
10
|
+
order_id: order_id
|
11
|
+
)
|
12
|
+
end
|
13
|
+
|
14
|
+
def complete!
|
15
|
+
return unless Payment::StoreCreditCard.new(tender, options).save!
|
16
|
+
|
17
|
+
transaction.response = handle_active_merchant_errors do
|
18
|
+
gateway.purchase(transaction.amount.cents, tender.token, options)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def order_id
|
25
|
+
tender.payment.id
|
26
|
+
end
|
27
|
+
|
28
|
+
def email
|
29
|
+
return unless tender.profile.present?
|
30
|
+
|
31
|
+
tender.profile.email
|
32
|
+
end
|
33
|
+
|
34
|
+
def billing_address
|
35
|
+
{
|
36
|
+
name: nil,
|
37
|
+
company: nil,
|
38
|
+
address1: address.street,
|
39
|
+
address2: address.street_2,
|
40
|
+
city: address.city,
|
41
|
+
state: address.region,
|
42
|
+
country: address.country.try(:alpha2),
|
43
|
+
zip: address.postal_code,
|
44
|
+
phone: nil
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Payment::Refund::CreditCard, with: :cyber_source do
|
3
|
+
def complete!
|
4
|
+
validate_reference!
|
5
|
+
|
6
|
+
transaction.response = handle_active_merchant_errors do
|
7
|
+
gateway.refund(
|
8
|
+
transaction.amount.cents,
|
9
|
+
transaction.reference.response.authorization
|
10
|
+
)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Workarea
|
2
|
+
decorate Payment::StoreCreditCard, with: :cyber_source do
|
3
|
+
def perform!
|
4
|
+
return true if @credit_card.token.present?
|
5
|
+
|
6
|
+
response = handle_active_merchant_errors do
|
7
|
+
gateway.store(@credit_card.to_active_merchant, @options)
|
8
|
+
end
|
9
|
+
|
10
|
+
@credit_card.token = response.authorization
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
data/bin/rails
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This command will automatically be run when you run "rails" with Rails gems
|
3
|
+
# installed from the root of your application.
|
4
|
+
|
5
|
+
ENGINE_ROOT = File.expand_path("../..", __FILE__)
|
6
|
+
ENGINE_PATH = File.expand_path("../../lib/workarea/cyber_source/engine", __FILE__)
|
7
|
+
APP_PATH = File.expand_path("../../test/dummy/config/application", __FILE__)
|
8
|
+
|
9
|
+
# Set up gems listed in the Gemfile.
|
10
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
|
11
|
+
require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"])
|
12
|
+
|
13
|
+
require "action_controller/railtie"
|
14
|
+
require "action_view/railtie"
|
15
|
+
require "action_mailer/railtie"
|
16
|
+
require "rails/test_unit/railtie"
|
17
|
+
require "sprockets/railtie"
|
18
|
+
require "teaspoon-mocha"
|
19
|
+
|
20
|
+
require "rails/engine/commands"
|
@@ -0,0 +1 @@
|
|
1
|
+
Workarea::CyberSource.auto_configure_gateway
|
@@ -0,0 +1,97 @@
|
|
1
|
+
module ActiveMerchant
|
2
|
+
module Billing
|
3
|
+
class BogusCyberSourceGateway < BogusGateway
|
4
|
+
def store(paysource, options = {})
|
5
|
+
authorization = ";5118839074516426404010;Ahj/7wSTFX4pmYA85bCqKhDdq4ZOWjNklxakXpGAKXFqRekZpABykhk0kyro9JiuKBOTFX4pmYA85bCqAAAA5wQ4;store;;;5118839074516426404010"
|
6
|
+
case normalize(paysource)
|
7
|
+
when /1$/
|
8
|
+
Response.new(true, SUCCESS_MESSAGE, { billingid: "1" }, { test: true, authorization: authorization })
|
9
|
+
when /2$/
|
10
|
+
Response.new(false, FAILURE_MESSAGE, { billingid: nil, error: FAILURE_MESSAGE }, { test: true, error_code: STANDARD_ERROR_CODE[:processing_error] })
|
11
|
+
else
|
12
|
+
raise Error, error_message(paysource)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def authorize(money, credit_card_or_subscription, options = {})
|
17
|
+
case normalize(credit_card_or_subscription)
|
18
|
+
when /1$/, "5118839074516426404010"
|
19
|
+
succuessful_auth_response
|
20
|
+
when /2$/
|
21
|
+
Response.new(false, FAILURE_MESSAGE, { authorized_amount: money, error: FAILURE_MESSAGE }, { test: true, error_code: STANDARD_ERROR_CODE[:processing_error] })
|
22
|
+
else
|
23
|
+
raise Error, error_message(credit_card_or_subscription)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def purchase(money, credit_card_or_subscription, options = {})
|
28
|
+
case normalize(credit_card_or_subscription)
|
29
|
+
when /1$/, "5118839074516426404010"
|
30
|
+
succuessful_purchase_response
|
31
|
+
when /2$/
|
32
|
+
Response.new(false, FAILURE_MESSAGE, { authorized_amount: money, error: FAILURE_MESSAGE }, { test: true, error_code: STANDARD_ERROR_CODE[:processing_error] })
|
33
|
+
else
|
34
|
+
raise Error, error_message(credit_card_or_subscription)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def normalize(paysource)
|
41
|
+
if paysource.respond_to?(:account_number) && (paysource.try(:number).blank? || paysource.number.blank?)
|
42
|
+
paysource.account_number
|
43
|
+
elsif paysource.respond_to?(:number)
|
44
|
+
paysource.number.split(";")[6] || paysource.number
|
45
|
+
else
|
46
|
+
paysource.to_s.split(";")[6] || paysource.to_s
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def succuessful_auth_response
|
51
|
+
authorization = "5a216efd87c68b548a1856df;5118839077736067504008;Ahj/7wSTFX4pnG3edheIKhDdq4ZOWbVqlwethj1gKXB62GPXpAn6OUkMmkmVdHpMVxQJyYq/FM427zsLxAAA6xxJ;authorize;500;;"
|
52
|
+
params = {
|
53
|
+
"merchantReferenceCode" => "5a1d848387c68b268bf205f9",
|
54
|
+
"requestID" => "5118839077736067504008",
|
55
|
+
"decision" => "ACCEPT",
|
56
|
+
"reasonCode" => "100",
|
57
|
+
"message" => "Successful transaction",
|
58
|
+
"requestToken" => "Ahj/7wSTFX4pnG3edheIKhDdq4ZOWbVqlwethj1gKXB62GPXpAn6OUkMmkmVdHpMVxQJyYq/FM427zsLxAAA6xxJ",
|
59
|
+
"currency" => "USD",
|
60
|
+
"amount" => "5.00",
|
61
|
+
"authorizationCode" => "831000",
|
62
|
+
"avsCode" => "Y",
|
63
|
+
"avsCodeRaw" => "Y",
|
64
|
+
"authorizedDateTime" => "2017-11-28T15:45:07Z",
|
65
|
+
"processorResponse" => "000",
|
66
|
+
"paymentNetworkTransactionID" => "558196000003814",
|
67
|
+
"cardCategory" => "A",
|
68
|
+
"ownerMerchantID" => "weblinc"
|
69
|
+
}
|
70
|
+
Response.new(true, SUCCESS_MESSAGE, params, test: true, authorization: authorization)
|
71
|
+
end
|
72
|
+
|
73
|
+
def succuessful_purchase_response
|
74
|
+
authorization = "5a609ec187c68b520db767eb;5161970476206134104008;Ahj//wSTF9S7HtFYm4/IESDdm1ZtGbZvKhyKjhnKtJcl3RY7YClyXdFjt6QJ+j6jDJpJl6MVzT24YE5MX1Lse0Vibj8g0UyQ;purchase;500;;"
|
75
|
+
params = {
|
76
|
+
"merchantReferenceCode" => "5a5f54b787c68be6e7c6a553",
|
77
|
+
"requestID" => "5161970476206134104008",
|
78
|
+
"decision" => "ACCEPT",
|
79
|
+
"reasonCode" => "100",
|
80
|
+
"message" => "Successful transaction",
|
81
|
+
"requestToken" => "Ahj//wSTF9S7HtFYm4/IESDdm1ZtGbZvKhyKjhnKtJcl3RY7YClyXdFjt6QJ+j6jDJpJl6MVzT24YE5MX1Lse0Vibj8g0UyQ",
|
82
|
+
"currency" => "USD",
|
83
|
+
"amount" => "5.00",
|
84
|
+
"authorizationCode" => "888888",
|
85
|
+
"avsCode" => "X",
|
86
|
+
"avsCodeRaw" => "I1",
|
87
|
+
"authorizedDateTime" => "2018-01-17T13:50:47Z",
|
88
|
+
"processorResponse" => "100",
|
89
|
+
"reconciliationID" => "73534367JCHT83JZ",
|
90
|
+
"ownerMerchantID" => "a",
|
91
|
+
"requestDateTime" => "2018-01-17T13:50:47Z"
|
92
|
+
}
|
93
|
+
Response.new(true, SUCCESS_MESSAGE, params, test: true, authorization: authorization)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
ActiveMerchant::Billing::CyberSourceGateway.class_eval do
|
2
|
+
def add_payment_method_or_subscription(xml, money, payment_method_or_reference, options)
|
3
|
+
if payment_method_or_reference.is_a?(String)
|
4
|
+
add_address(xml, nil, options[:billing_address], options)
|
5
|
+
add_purchase_data(xml, money, true, options)
|
6
|
+
add_subscription(xml, options, payment_method_or_reference)
|
7
|
+
elsif card_brand(payment_method_or_reference) == 'check'
|
8
|
+
add_address(xml, payment_method_or_reference, options[:billing_address], options)
|
9
|
+
add_purchase_data(xml, money, true, options)
|
10
|
+
add_check(xml, payment_method_or_reference)
|
11
|
+
else
|
12
|
+
add_address(xml, payment_method_or_reference, options[:billing_address], options)
|
13
|
+
add_address(xml, payment_method_or_reference, options[:shipping_address], options, true)
|
14
|
+
add_purchase_data(xml, money, true, options)
|
15
|
+
add_creditcard(xml, payment_method_or_reference)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_void_request(identification, options)
|
20
|
+
order_id, request_id, request_token, action, money, currency = identification.split(";")
|
21
|
+
options[:order_id] = order_id
|
22
|
+
|
23
|
+
xml = Builder::XmlMarkup.new :indent => 2
|
24
|
+
# normal active merchant only has if capture, but purchases should be the same as captures
|
25
|
+
# a pr was submited to active merchant, remove this if it ever gets mergex / fixed upstream
|
26
|
+
if action == "capture" || action == "purchase"
|
27
|
+
add_void_service(xml, request_id, request_token)
|
28
|
+
else
|
29
|
+
add_purchase_data(xml, money, true, options.merge(:currency => currency || default_currency))
|
30
|
+
add_auth_reversal_service(xml, request_id, request_token)
|
31
|
+
end
|
32
|
+
xml.target!
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "workarea"
|
2
|
+
require "workarea/storefront"
|
3
|
+
require "workarea/admin"
|
4
|
+
require "active_merchant/billing/bogus_cyber_source_gateway"
|
5
|
+
require "active_merchant/billing/cyber_source_fix"
|
6
|
+
|
7
|
+
require "workarea/cyber_source/engine"
|
8
|
+
require "workarea/cyber_source/version"
|
9
|
+
|
10
|
+
module Workarea
|
11
|
+
module CyberSource
|
12
|
+
def self.auto_configure_gateway
|
13
|
+
if Rails.application.secrets.cyber_source.present?
|
14
|
+
if ENV["HTTP_PROXY"].present?
|
15
|
+
uri = URI.parse(ENV["HTTP_PROXY"])
|
16
|
+
ActiveMerchant::Billing::CyberSourceGateway.proxy_address = uri.host
|
17
|
+
ActiveMerchant::Billing::CyberSourceGateway.proxy_port = uri.port
|
18
|
+
end
|
19
|
+
|
20
|
+
self.gateway = ActiveMerchant::Billing::CyberSourceGateway.new(
|
21
|
+
Rails.application.secrets.cyber_source.deep_symbolize_keys
|
22
|
+
)
|
23
|
+
else
|
24
|
+
self.gateway = ActiveMerchant::Billing::BogusCyberSourceGateway.new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.gateway
|
29
|
+
Workarea.config.gateways.credit_card
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.gateway=(gateway)
|
33
|
+
Workarea.config.gateways.credit_card = gateway
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/test/dummy/Rakefile
ADDED
data/test/dummy/bin/rake
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "pathname"
|
3
|
+
require "fileutils"
|
4
|
+
include FileUtils
|
5
|
+
|
6
|
+
# path to your application root.
|
7
|
+
APP_ROOT = Pathname.new File.expand_path("../../", __FILE__)
|
8
|
+
|
9
|
+
def system!(*args)
|
10
|
+
system(*args) || abort("\n== Command #{args} failed ==")
|
11
|
+
end
|
12
|
+
|
13
|
+
chdir APP_ROOT do
|
14
|
+
# This script is a starting point to setup your application.
|
15
|
+
# Add necessary setup steps to this file.
|
16
|
+
|
17
|
+
puts "== Installing dependencies =="
|
18
|
+
system! "gem install bundler --conservative"
|
19
|
+
system("bundle check") || system!("bundle install")
|
20
|
+
|
21
|
+
# Install JavaScript dependencies if using Yarn
|
22
|
+
# system('bin/yarn')
|
23
|
+
|
24
|
+
|
25
|
+
# puts "\n== Copying sample files =="
|
26
|
+
# unless File.exist?('config/database.yml')
|
27
|
+
# cp 'config/database.yml.sample', 'config/database.yml'
|
28
|
+
# end
|
29
|
+
|
30
|
+
puts "\n== Preparing database =="
|
31
|
+
system! "bin/rails db:setup"
|
32
|
+
|
33
|
+
puts "\n== Removing old logs and tempfiles =="
|
34
|
+
system! "bin/rails log:clear tmp:clear"
|
35
|
+
|
36
|
+
puts "\n== Restarting application server =="
|
37
|
+
system! "bin/rails restart"
|
38
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "pathname"
|
3
|
+
require "fileutils"
|
4
|
+
include FileUtils
|
5
|
+
|
6
|
+
# path to your application root.
|
7
|
+
APP_ROOT = Pathname.new File.expand_path("../../", __FILE__)
|
8
|
+
|
9
|
+
def system!(*args)
|
10
|
+
system(*args) || abort("\n== Command #{args} failed ==")
|
11
|
+
end
|
12
|
+
|
13
|
+
chdir APP_ROOT do
|
14
|
+
# This script is a way to update your development environment automatically.
|
15
|
+
# Add necessary update steps to this file.
|
16
|
+
|
17
|
+
puts "== Installing dependencies =="
|
18
|
+
system! "gem install bundler --conservative"
|
19
|
+
system("bundle check") || system!("bundle install")
|
20
|
+
|
21
|
+
puts "\n== Updating database =="
|
22
|
+
system! "bin/rails db:migrate"
|
23
|
+
|
24
|
+
puts "\n== Removing old logs and tempfiles =="
|
25
|
+
system! "bin/rails log:clear tmp:clear"
|
26
|
+
|
27
|
+
puts "\n== Restarting application server =="
|
28
|
+
system! "bin/rails restart"
|
29
|
+
end
|