diff --git a/.ruby-gemset b/.ruby-gemset
index 4075d6b..447ff17 100644
--- a/.ruby-gemset
+++ b/.ruby-gemset
@@ -1 +1 @@
-braintree_rails_example
+braintree_graphql_rails_example
diff --git a/Dockerfile b/Dockerfile
index 88286fd..d0f3880 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,7 @@ FROM ruby:2.3.1-onbuild
RUN apt-get update && apt-get install -y build-essential nodejs
-ENV APP_HOME /braintree_rails_example
+ENV APP_HOME /braintree_graphql_rails_example
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
diff --git a/Gemfile b/Gemfile
index e6afea0..8c21813 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,10 +22,10 @@ gem 'jbuilder', '~> 2.0'
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', '~> 0.4.0', group: :doc
-gem 'braintree', '~> 2.87'
-
gem 'dotenv', '~> 2.0'
+gem 'httparty', '~> 0.16.2'
+
group :development, :test do
# Use sqlite in development and test for ease of setup
gem 'sqlite3'
diff --git a/Gemfile.lock b/Gemfile.lock
index 29e1afe..5fe9f73 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -47,8 +47,6 @@ GEM
debug_inspector (>= 0.0.1)
bootsnap (1.3.2)
msgpack (~> 1.0)
- braintree (2.94.0)
- builder (>= 2.0.0)
builder (3.2.3)
byebug (10.0.2)
coffee-rails (4.2.2)
@@ -68,7 +66,10 @@ GEM
ffi (1.9.25)
globalid (0.4.2)
activesupport (>= 4.2.0)
- i18n (1.6.0)
+ httparty (0.16.2)
+ concurrent-ruby (~> 1.0)
+ multi_xml (>= 0.5.2)
+ i18n (1.7.0)
concurrent-ruby (~> 1.0)
jbuilder (2.8.0)
activesupport (>= 4.2.0)
@@ -96,6 +97,7 @@ GEM
minitest (5.11.3)
msgpack (1.2.10)
multi_json (1.13.1)
+ multi_xml (0.6.0)
nio4r (2.3.1)
nokogiri (1.10.5)
mini_portile2 (~> 2.4.0)
@@ -207,10 +209,10 @@ PLATFORMS
DEPENDENCIES
bootsnap (= 1.3.2)
- braintree (~> 2.87)
byebug
coffee-rails (~> 4.2.2)
dotenv (~> 2.0)
+ httparty (~> 0.16.2)
jbuilder (~> 2.0)
jquery-rails
listen (~> 3.1, >= 3.1.5)
diff --git a/README.md b/README.md
index 2a2eb56..6d7000d 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-# Braintree Rails Example
+# Braintree GraphQL Example in Rails
-[![Build Status](https://travis-ci.org/braintree/braintree_rails_example.svg?branch=master)](https://travis-ci.org/braintree/braintree_rails_example)
+[![Build Status](https://travis-ci.org/braintree/braintree_graphql_rails_example.svg?branch=master)](https://travis-ci.org/braintree/braintree_graphql_rails_example)
-An example Braintree integration for Ruby on Rails.
+An example Braintree integration with the [GraphQL API](https://graphql.braintreepayments.com/) using Ruby on Rails. Forked from [braintree/braintree_rails_example](https://github.com/braintree/braintree_rails_example).
## Setup Instructions
@@ -30,7 +30,7 @@ An example Braintree integration for Ruby on Rails.
You can deploy this app directly to Heroku to see the app live. Skip the setup instructions above and click the button below. This will walk you through getting this app up and running on Heroku in minutes.
-[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/braintree/braintree_rails_example&env[BT_ENVIRONMENT]=sandbox)
+[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/braintree/braintree_graphql_rails_example&env[BT_VERSION]=2019-11-11)
## Running Tests
@@ -57,7 +57,7 @@ Sandbox transactions must be made with [sample credit card numbers](https://deve
## Help
- * Found a bug? Have a suggestion for improvement? Want to tell us we're awesome? [Submit an issue](https://github.com/braintree/braintree_rails_example/issues)
+ * Found a bug? Have a suggestion for improvement? Want to tell us we're awesome? [Submit an issue](https://github.com/braintree/braintree_graphql_rails_example/issues)
* Trouble with your integration? Contact [Braintree Support](https://support.braintreepayments.com/) / support@braintreepayments.com
* Want to contribute? [Submit a pull request](https://help.github.com/articles/creating-a-pull-request)
diff --git a/app.json b/app.json
index 857740a..399e9f5 100644
--- a/app.json
+++ b/app.json
@@ -1,21 +1,12 @@
{
- "name": "Braintree Rails Example",
- "description": "An example Braintree integration for Ruby on Rails",
- "keywords": ["braintree", "ruby", "rails"],
+ "name": "Braintree GraphQL Example in Rails",
+ "description": "An example Braintree integration with the GraphQL API using Ruby on Rails",
+ "keywords": ["braintree", "graphql", "ruby", "rails"],
"website": "https://www.braintreepayments.com",
- "repository": "https://github.com/braintree/braintree_rails_example",
+ "repository": "https://github.com/braintree/braintree_graphql_rails_example",
"logo": "https://avatars1.githubusercontent.com/u/3453",
"success_url": "/",
"env": {
- "BT_ENVIRONMENT": {
- "description": "Run against Braintree sandbox environment",
- "required": true,
- "value": "sandbox"
- },
- "BT_MERCHANT_ID": {
- "description": "Your Braintree Merchant ID",
- "required": true
- },
"BT_PUBLIC_KEY": {
"description": "Your Braintree Public Key",
"required": true
@@ -23,6 +14,10 @@
"BT_PRIVATE_KEY": {
"description": "Your Braintree Private Key",
"required": true
+ },
+ "BT_VERSION": {
+ "description": "A date in the format YYYY-MM-DD (we recommend using the date you started using the GraphQL API)",
+ "required": true
}
}
}
diff --git a/app/assets/stylesheets/app.css.erb b/app/assets/stylesheets/app.css.erb
index f7c12b5..b50a40f 100644
--- a/app/assets/stylesheets/app.css.erb
+++ b/app/assets/stylesheets/app.css.erb
@@ -2152,10 +2152,10 @@ td {
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-word;
- -ms-hyphens: auto;
- -moz-hyphens: auto;
- -webkit-hyphens: auto;
- hyphens: auto;
+ -ms-hyphens: none;
+ -moz-hyphens: none;
+ -webkit-hyphens: none;
+ hyphens: none;
line-height: 1.3em;
font-size: 14px;
letter-spacing: .02em;
diff --git a/app/controllers/checkouts_controller.rb b/app/controllers/checkouts_controller.rb
index 8cb8d3b..e71aa2b 100644
--- a/app/controllers/checkouts_controller.rb
+++ b/app/controllers/checkouts_controller.rb
@@ -1,46 +1,48 @@
class CheckoutsController < ApplicationController
TRANSACTION_SUCCESS_STATUSES = [
- Braintree::Transaction::Status::Authorizing,
- Braintree::Transaction::Status::Authorized,
- Braintree::Transaction::Status::Settled,
- Braintree::Transaction::Status::SettlementConfirmed,
- Braintree::Transaction::Status::SettlementPending,
- Braintree::Transaction::Status::Settling,
- Braintree::Transaction::Status::SubmittedForSettlement,
+ "AUTHORIZED",
+ "AUTHORIZING",
+ "SETTLED",
+ "SETTLEMENT_PENDING",
+ "SETTLING",
+ "SUBMITTED_FOR_SETTLEMENT",
]
def new
- @client_token = gateway.client_token.generate
+ @client_token = gateway.client_token.dig("data", "createClientToken", "clientToken")
end
def show
- @transaction = gateway.transaction.find(params[:id])
- @result = _create_result_hash(@transaction)
+ begin
+ @transaction = gateway.node_fetch_transaction(params[:id]).fetch("data", {})["transaction"]
+ @result = _create_status_result_hash(@transaction)
+ rescue BraintreeGateway::GraphQLError => error
+ _flash_errors(error)
+ redirect_to new_checkout_path
+ end
end
def create
amount = params["amount"] # In production you should not take amounts directly from clients
nonce = params["payment_method_nonce"]
- result = gateway.transaction.sale(
- amount: amount,
- payment_method_nonce: nonce,
- :options => {
- :submit_for_settlement => true
- }
- )
+ begin
+ result = gateway.transaction(nonce, amount)
+ id = result.dig("data", "chargePaymentMethod", "transaction", "id")
- if result.success? || result.transaction
- redirect_to checkout_path(result.transaction.id)
- else
- error_messages = result.errors.map { |error| "Error: #{error.code}: #{error.message}" }
- flash[:error] = error_messages
+ if id
+ redirect_to checkout_path(id)
+ else
+ raise BraintreeGateway::GraphQLError.new(result)
+ end
+ rescue BraintreeGateway::GraphQLError => error
+ _flash_errors(error)
redirect_to new_checkout_path
end
end
- def _create_result_hash(transaction)
- status = transaction.status
+ def _create_status_result_hash(transaction)
+ status = transaction["status"]
if TRANSACTION_SUCCESS_STATUSES.include? status
result_hash = {
@@ -50,21 +52,22 @@ def _create_result_hash(transaction)
}
else
result_hash = {
- :header => "Transaction Failed",
+ :header => "Transaction Unsuccessful",
:icon => "fail",
:message => "Your test transaction has a status of #{status}. See the Braintree API response and try again."
}
end
end
- def gateway
- env = ENV["BT_ENVIRONMENT"]
+ def _flash_errors(error)
+ if error.messages != nil and !error.messages.empty?
+ flash[:error] = error.messages
+ else
+ flash[:error] = ["Error: Something unexpected went wrong! Try again."]
+ end
+ end
- @gateway ||= Braintree::Gateway.new(
- :environment => env && env.to_sym,
- :merchant_id => ENV["BT_MERCHANT_ID"],
- :public_key => ENV["BT_PUBLIC_KEY"],
- :private_key => ENV["BT_PRIVATE_KEY"],
- )
+ def gateway
+ @gateway ||= BraintreeGateway.new(HTTParty)
end
end
diff --git a/app/models/braintree_gateway.rb b/app/models/braintree_gateway.rb
new file mode 100644
index 0000000..cf4185c
--- /dev/null
+++ b/app/models/braintree_gateway.rb
@@ -0,0 +1,160 @@
+class BraintreeGateway
+ LOGGER = ::Logger.new(STDOUT)
+ ENDPOINT = "https://payments.sandbox.braintree-api.com/graphql"
+ CONTENT_TYPE = "application/json"
+ VERSION = ENV["BT_VERSION"]
+ BASIC_AUTH_USERNAME = ENV["BT_PUBLIC_KEY"]
+ BASIC_AUTH_PASSWORD = ENV["BT_PRIVATE_KEY"]
+
+ def initialize(requester_class)
+ @requester = requester_class
+ end
+
+ def ping
+ _make_request("ping", "{ ping }")
+ end
+
+ def client_token
+ operation_name = "createClientToken"
+ result = _make_request(operation_name, "mutation { #{operation_name}(input: {}) { clientToken } }")
+ end
+
+ def transaction(payment_method_id, amount)
+ operation_name = "chargePaymentMethod"
+ query = <<~GRAPHQL
+ mutation($input: ChargePaymentMethodInput!) {
+ #{operation_name}(input: $input) {
+ transaction {
+ id
+ }
+ }
+ }
+ GRAPHQL
+ variables = {
+ :input => {
+ :paymentMethodId => payment_method_id,
+ :transaction => {
+ :amount => amount,
+ },
+ }
+ }
+
+ _make_request(operation_name, query, variables)
+ end
+
+ def vault(single_use_payment_method_id)
+ operation_name = "vaultPaymentMethod"
+ _make_request(
+ operation_name,
+ "mutation($input: VaultPaymentMethodInput!) { #{operation_name}(input: $input) { paymentMethod { id usage } } }",
+ {:input => {
+ :paymentMethodId => single_use_payment_method_id,
+ }}
+ )
+ end
+
+ def node_fetch_transaction(transaction_id)
+ operation_name = "transaction"
+ query = <<~GRAPHQL
+ query {
+ #{operation_name}:node(id: "#{transaction_id}") {
+ ... on Transaction {
+ id
+ amount { value currencyIsoCode }
+ status
+ createdAt
+ paymentMethodSnapshot {
+ __typename
+ ... on CreditCardDetails {
+ bin
+ brandCode
+ cardholderName
+ expirationMonth
+ expirationYear
+ last4
+ binData { countryOfIssuance }
+ origin { type }
+ }
+ ... on PayPalTransactionDetails {
+ payer {
+ email
+ payerId
+ firstName
+ lastName
+ }
+ payerStatus
+ }
+ }
+ }
+ }
+ }
+ GRAPHQL
+ _make_request(operation_name, query)
+ end
+
+ def _generate_payload(query_string, variables_hash)
+ JSON.generate({
+ :query => query_string,
+ :variables => variables_hash
+ }).to_s
+ end
+
+ def _make_request(operation_name, query_string, variables_hash = {})
+ # rescue http exceptions thrown by httparty and throw a GraphQLError
+ payload = _generate_payload(query_string, variables_hash)
+ result = @requester.post(
+ ENDPOINT,
+ {
+ :body => payload,
+ :basic_auth => {
+ :username => BASIC_AUTH_USERNAME,
+ :password => BASIC_AUTH_PASSWORD,
+ },
+ :headers => {
+ "Braintree-Version" => VERSION,
+ "Content-Type" => CONTENT_TYPE,
+ },
+ :logger => LOGGER,
+ :log_level => :debug,
+ }
+ ).parsed_response
+ is_any_data_present = (result["data"] != nil and result["data"][operation_name] != nil)
+
+ if result["errors"]
+ LOGGER.error(
+ <<~SEMISTRUCTUREDLOG
+ "top_level_message" => "Error present on GraphQL request to Braintree.",
+ "operation_name" => #{operation_name},
+ "braintree_request_id" => #{self.class.parse_braintree_request_id(result)},
+ "result" => #{result},
+ "request" => #{payload}"
+ SEMISTRUCTUREDLOG
+ )
+ end
+
+ if is_any_data_present
+ return result
+ else
+ raise GraphQLError.new(result)
+ end
+ end
+
+ def self.parse_braintree_request_id(result)
+ result.dig("extensions", "requestId")
+ end
+
+ class GraphQLError < StandardError
+ attr_reader :messages
+ def initialize(graphql_result)
+ @messages = graphql_result["errors"].map { |error| "Error: " + error["message"] } if graphql_result["errors"]
+
+ LOGGER.error(
+ <<~SEMISTRUCTUREDLOG
+ "top_level_message" => #{@messages.to_s},
+ "braintree_request_id" => #{BraintreeGateway.parse_braintree_request_id(graphql_result)},
+ "result" => #{graphql_result},
+ SEMISTRUCTUREDLOG
+ )
+ end
+ end
+end
diff --git a/app/views/checkouts/_credit_card_details.html.erb b/app/views/checkouts/_credit_card_details.html.erb
new file mode 100644
index 0000000..67e5414
--- /dev/null
+++ b/app/views/checkouts/_credit_card_details.html.erb
@@ -0,0 +1,42 @@
+<% credit_card = locals[:credit_card] %>
+
by Credit Card
+
+
+
+ card type |
+ <%= credit_card["brandCode"] %> |
+
+
+ bin |
+ <%= credit_card["bin"] %> |
+
+
+ last 4 |
+ <%= credit_card["last4"] %> |
+
+
+ expiration month |
+ <%= credit_card["expirationMonth"] %> |
+
+
+ expiration year |
+ <%= credit_card["expirationYear"] %> |
+
+
+ cardholder name |
+ <%= credit_card["cardholderName"] %> |
+
+
+ card issued in |
+ <%= credit_card.fetch("binData", {})["countryOfIssuance"] %> |
+
+
+ card sourced from |
+ <% if credit_card["origin"] %>
+ <%= credit_card["origin"]["type"] %> |
+ <% else %>
+ customer entering card details directly |
+ <% end %>
+
+
+
diff --git a/app/views/checkouts/_paypal_account_details.html.erb b/app/views/checkouts/_paypal_account_details.html.erb
new file mode 100644
index 0000000..0820d94
--- /dev/null
+++ b/app/views/checkouts/_paypal_account_details.html.erb
@@ -0,0 +1,28 @@
+<% paypal_account = locals[:paypal_account] %>
+by PayPal Account
+
+
+ <% if paypal_account["payer"] %>
+
+ email |
+ <%= paypal_account["payer"]["email"] %> |
+
+
+ payer ID |
+ <%= paypal_account["payer"]["payerId"] %> |
+
+
+ first name |
+ <%= paypal_account["payer"]["firstName"] %> |
+
+
+ last name |
+ <%= paypal_account["payer"]["lastName"] %> |
+
+ <% end %>
+
+ payer status |
+ <%= paypal_account["payerStatus"] %> |
+
+
+
diff --git a/app/views/checkouts/show.html.erb b/app/views/checkouts/show.html.erb
index c244d36..6038638 100644
--- a/app/views/checkouts/show.html.erb
+++ b/app/views/checkouts/show.html.erb
@@ -42,118 +42,45 @@
id |
- <%= @transaction.id %> |
-
-
- type |
- <%= @transaction.type %> |
+ <%= @transaction["id"] %> |
amount |
- <%= @transaction.amount %> |
-
-
- status |
- <%= @transaction.status %> |
-
-
- created_at |
- <%= @transaction.created_at %> |
-
-
- updated_at |
- <%= @transaction.updated_at %> |
-
-
-
-
-
-
- Payment
-
-
-
-
- token |
- <%= @transaction.credit_card_details.token %> |
-
-
- bin |
- <%= @transaction.credit_card_details.bin %> |
-
-
- last_4 |
- <%= @transaction.credit_card_details.last_4 %> |
-
-
- card_type |
- <%= @transaction.credit_card_details.card_type %> |
+ <%= @transaction["amount"]["value"] + " " + @transaction["amount"]["currencyIsoCode"] if @transaction["amount"] %> |
- expiration_date |
- <%= @transaction.credit_card_details.expiration_date %> |
+ current status |
+ <%= @transaction["status"] %> |
- cardholder_name |
- <%= @transaction.credit_card_details.cardholder_name %> |
-
-
- customer_location |
- <%= @transaction.credit_card_details.customer_location %> |
+ created at |
+ <%= @transaction["createdAt"] %> |
- <% if @transaction.customer_details.id %>
+ <% if @transaction["paymentMethodSnapshot"] %>
- Customer
-
-
-
-
- id |
- <%= @transaction.customer_details.id %> |
-
-
- first_name |
- <%= @transaction.customer_details.first_name %> |
-
-
- last_name |
- <%= @transaction.customer_details.last_name %> |
-
-
- email |
- <%= @transaction.customer_details.email %> |
-
-
- company |
- <%= @transaction.customer_details.company %> |
-
-
- website |
- <%= @transaction.customer_details.website %> |
-
-
- phone |
- <%= @transaction.customer_details.phone %> |
-
-
- fax |
- <%= @transaction.customer_details.fax %> |
-
-
-
+ Payment
+ <% if @transaction["paymentMethodSnapshot"]["__typename"] == "CreditCardDetails" %>
+ <%= render "checkouts/credit_card_details", :locals => {:credit_card => @transaction["paymentMethodSnapshot"]} %>
+ <% end %>
+ <% if @transaction["paymentMethodSnapshot"]["__typename"] == "PayPalTransactionDetails" %>
+ <%= render "checkouts/paypal_account_details", :locals => {:paypal_account => @transaction["paymentMethodSnapshot"]} %>
+ <% end %>
<% end %>
- Integrate with the Braintree SDK for a secure and seamless checkout
+ Integrate with the Braintree JS SDK and GraphQL API for a secure and seamless checkout
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 03a0de8..b27c257 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -1,7 +1,7 @@
- BraintreeRailsExample
+ BraintreeGraphQLRailsExample
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
<%= favicon_link_tag "favicon.png" %>
diff --git a/config/application.rb b/config/application.rb
index a1a19d6..1f70dde 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -17,7 +17,7 @@
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
-module BraintreeRailsExample
+module BraintreeGraphQLRailsExample
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
config.load_defaults 5.2
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 0e4f88b..fcc8242 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -56,7 +56,7 @@
# Use a real queuing backend for Active Job (and separate queues per environment)
# config.active_job.queue_adapter = :resque
- # config.active_job.queue_name_prefix = "braintree_rails_example_#{Rails.env}"
+ # config.active_job.queue_name_prefix = "braintree_graphql_rails_example_#{Rails.env}"
config.action_mailer.perform_caching = false
diff --git a/config/initializers/braintree.rb b/config/initializers/braintree.rb
index d57630e..d9e3a37 100644
--- a/config/initializers/braintree.rb
+++ b/config/initializers/braintree.rb
@@ -1,5 +1,5 @@
Dotenv.load
-if !ENV["BT_ENVIRONMENT"] || !ENV["BT_MERCHANT_ID"] || !ENV["BT_PUBLIC_KEY"] || !ENV["BT_PRIVATE_KEY"]
- raise "Cannot find necessary environmental variables. See https://github.com/braintree/braintree_rails_example#setup-instructions for instructions";
+if !ENV["BT_PUBLIC_KEY"] || !ENV["BT_PRIVATE_KEY"] || !ENV["BT_VERSION"]
+ raise "Cannot find necessary environmental variables. See https://github.com/braintree/braintree_graphql_rails_example#setup-instructions for instructions";
end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index cc6295f..5314071 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -1,3 +1,3 @@
# Be sure to restart your server when you modify this file.
-Rails.application.config.session_store :cookie_store, key: '_braintree_rails_example_session'
+Rails.application.config.session_store :cookie_store, key: '_braintree_graphql_rails_example_session'
diff --git a/docker-compose.yml b/docker-compose.yml
index b51e3d3..a527e3f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -6,4 +6,4 @@ services:
ports:
- "4567:4567"
volumes:
- - .:/braintree_rails_example
+ - .:/braintree_graphql_rails_example
diff --git a/example.env b/example.env
index 490e7f8..6370e8a 100644
--- a/example.env
+++ b/example.env
@@ -1,4 +1,3 @@
-BT_ENVIRONMENT='sandbox'
-BT_MERCHANT_ID='your braintree merchant id'
BT_PUBLIC_KEY='your braintree public key'
BT_PRIVATE_KEY='your braintree private key'
+BT_VERSION='2018-11-11'
diff --git a/spec/controllers/checkouts_controller_spec.rb b/spec/controllers/checkouts_controller_spec.rb
index 49b97d2..ce796a4 100644
--- a/spec/controllers/checkouts_controller_spec.rb
+++ b/spec/controllers/checkouts_controller_spec.rb
@@ -6,10 +6,16 @@
include_context 'mock_data'
before do
- @mock_gateway = double("gateway")
- allow(@mock_gateway).to receive_message_chain("client_token.generate") { "your_client_token" }
+ @mock_gateway = instance_double("BraintreeGateway")
+ allow(@mock_gateway).to receive(:client_token).and_return({
+ "data" => {
+ "createClientToken" => {
+ "clientToken" => "your_client_token"
+ }
+ }
+ })
- expect(Braintree::Gateway).to receive(:new).and_return(@mock_gateway)
+ allow(BraintreeGateway).to receive(:new).and_return(@mock_gateway)
end
describe "GET #new" do
@@ -26,7 +32,7 @@
describe "GET #show" do
it "returns http success" do
- allow(@mock_gateway).to receive_message_chain("transaction.find") { mock_transaction }
+ allow(@mock_gateway).to receive(:node_fetch_transaction).and_return(mock_successful_fetched_transaction)
get :show, params: { id: "my_id" }
@@ -34,32 +40,24 @@
end
it "displays the transaction's fields" do
- allow(@mock_gateway).to receive_message_chain("transaction.find") { mock_transaction }
+ allow(@mock_gateway).to receive(:node_fetch_transaction).and_return(mock_successful_fetched_transaction)
get :show, params: { id: "my_id" }
expect(response.body).to match /my_id/
- expect(response.body).to match /sale/
- expect(response.body).to match /10\.0/
- expect(response.body).to match /authorized/
- expect(response.body).to match /ijkl/
+ expect(response.body).to match /12\.12/
+ expect(response.body).to match /CAD/
+ expect(response.body).to match /SUBMITTED_FOR_SETTLEMENT/
expect(response.body).to match /545454/
- expect(response.body).to match /5454/
- expect(response.body).to match /MasterCard/
- expect(response.body).to match /12\/2015/
- expect(response.body).to match /Bill Billson/
- expect(response.body).to match /US/
- expect(response.body).to match /h6hh3j/
- expect(response.body).to match /Bill/
- expect(response.body).to match /Billson/
- expect(response.body).to match /bill@example.com/
+ expect(response.body).to match /4444/
expect(response.body).to match /Billy Bobby Pins/
- expect(response.body).to match /bobby_pins.example.com/
- expect(response.body).to match /1234567890/
+ expect(response.body).to match /12/
+ expect(response.body).to match /2020/
+ expect(response.body).to match /USA/
end
it "populates result object with success for a succesful transaction" do
- allow(@mock_gateway).to receive_message_chain("transaction.find") { mock_transaction }
+ allow(@mock_gateway).to receive(:node_fetch_transaction).and_return(mock_successful_fetched_transaction)
get :show, params: { id: "my_id" }
@@ -72,15 +70,16 @@
it "populates result object with failure for a failed transaction" do
- allow(@mock_gateway).to receive_message_chain("transaction.find") { mock_failed_transaction }
+ allow(@mock_gateway).to receive(:node_fetch_transaction).and_return(mock_processor_decline_fetched_transaction)
get :show, params: { id: "my_id" }
expect(assigns(:result)).to eq({
- :header => "Transaction Failed",
+ :header => "Transaction Unsuccessful",
:icon => "fail",
- :message => "Your test transaction has a status of processor_declined. See the Braintree API response and try again."
+ :message => "Your test transaction has a status of PROCESSOR_DECLINED. See the Braintree API response and try again."
})
+ expect(response.body).to match /PROCESSOR_DECLINED/
end
end
@@ -89,44 +88,41 @@
amount = "10.00"
nonce = "fake-valid-nonce"
- allow(@mock_gateway).to receive_message_chain("transaction.sale") {
- Braintree::SuccessfulResult.new(transaction: mock_transaction)
- }
+ allow(@mock_gateway).to receive(:transaction).and_return(mock_created_transaction)
post :create, params: { payment_method_nonce: nonce, amount: amount }
- expect(response).to redirect_to("/checkouts/#{mock_transaction.id}")
+ expect(response).to redirect_to("/checkouts/#{mock_created_transaction["data"]["chargePaymentMethod"]["transaction"]["id"]}")
end
- context "when there are processor errors" do
- it "redirects to the transaction page" do
- amount = "2000"
+ context "when braintree returns an error" do
+ it "displays graphql errors" do
+ amount = "nine and three quarters"
nonce = "fake-valid-nonce"
- allow(@mock_gateway).to receive_message_chain("transaction.sale") {
- processor_declined_result
- }
+ allow(@mock_gateway).to receive(:transaction).and_raise(
+ BraintreeGateway::GraphQLError.new(mock_transaction_graphql_error)
+ )
post :create, params: { payment_method_nonce: nonce, amount: amount }
- expect(response).to redirect_to("/checkouts/#{processor_declined_result.transaction.id}")
+ expect(flash[:error]).to eq([
+ "Error: Variable 'amount' has an invalid value. Values of type Amount must contain exactly 0, 2 or 3 decimal places."
+ ])
end
- end
- context "when braintree returns an error" do
- it "displays the errors" do
- amount = "not_a_valid_amount"
- nonce = "not_a_valid_nonce"
+ it "displays validation errors" do
+ amount = "9.75"
+ nonce = "non-fake-invalid-nonce"
- allow(@mock_gateway).to receive_message_chain("transaction.sale") {
- sale_error_result
- }
+ allow(@mock_gateway).to receive(:transaction).and_raise(
+ BraintreeGateway::GraphQLError.new(mock_transaction_validation_error)
+ )
post :create, params: { payment_method_nonce: nonce, amount: amount }
expect(flash[:error]).to eq([
- "Error: 81503: Amount is an invalid format.",
- "Error: 91565: Unknown payment_method_nonce.",
+ "Error: Unknown or expired payment method ID.",
])
end
@@ -134,14 +130,33 @@
amount = "not_a_valid_amount"
nonce = "not_a_valid_nonce"
- allow(@mock_gateway).to receive_message_chain("transaction.sale") {
- sale_error_result
- }
+ allow(@mock_gateway).to receive(:transaction).and_raise(
+ BraintreeGateway::GraphQLError.new(mock_transaction_graphql_error)
+ )
post :create, params: { payment_method_nonce: nonce, amount: amount }
expect(response).to redirect_to(new_checkout_path)
end
+
+ it "gracefully handles unexpected errors" do
+ amount = "10.10"
+ nonce = "a-very-valid-nonce"
+
+ allow(@mock_gateway).to receive(:transaction).and_raise(
+ BraintreeGateway::GraphQLError.new({
+ "data" => nil,
+ "errors" => nil,
+ })
+ )
+
+ post :create, params: { payment_method_nonce: nonce, amount: amount }
+
+ expect(flash[:error]).to eq([
+ "Error: Something unexpected went wrong! Try again."
+ ])
+ expect(response).to redirect_to(new_checkout_path)
+ end
end
end
end
diff --git a/spec/integration/controllers/checkouts_controller_spec.rb b/spec/integration/controllers/checkouts_controller_spec.rb
index d99d2ad..7933204 100644
--- a/spec/integration/controllers/checkouts_controller_spec.rb
+++ b/spec/integration/controllers/checkouts_controller_spec.rb
@@ -28,26 +28,17 @@
it "retrieves the Braintree transaction and displays its attributes" do
# Using a random amount to prevent duplicate checking errors
amount = "#{random.rand(100)}.#{random.rand(100)}"
- result = gateway.transaction.sale(
- :amount => amount,
- :payment_method_nonce => "fake-valid-nonce",
- )
+ result = BraintreeGateway.new(HTTParty).transaction("fake-valid-nonce", amount)
+ expect(result["data"]["chargePaymentMethod"]).not_to be_nil
- expect(result).to be_success
- transaction = result.transaction
+ transaction = result["data"]["chargePaymentMethod"]["transaction"]
- get :show, params: { id: transaction.id }
+ get :show, params: { id: transaction["id"] }
expect(response).to have_http_status(:success)
- expect(response.body).to match Regexp.new(transaction.id)
- expect(response.body).to match Regexp.new(transaction.type)
- expect(response.body).to match Regexp.new(transaction.amount.to_s)
- expect(response.body).to match Regexp.new(transaction.status)
- expect(response.body).to match Regexp.new(transaction.credit_card_details.bin)
- expect(response.body).to match Regexp.new(transaction.credit_card_details.last_4)
- expect(response.body).to match Regexp.new(transaction.credit_card_details.card_type)
- expect(response.body).to match Regexp.new(transaction.credit_card_details.expiration_date)
- expect(response.body).to match Regexp.new(transaction.credit_card_details.customer_location)
+ expect(response.body).to match Regexp.new(transaction["id"])
+ expect(response.body).to match Regexp.new(amount)
+ expect(response.body).to match "SUBMITTED_FOR_SETTLEMENT"
end
end
diff --git a/spec/models/braintree_gateway_spec.rb b/spec/models/braintree_gateway_spec.rb
new file mode 100644
index 0000000..697375a
--- /dev/null
+++ b/spec/models/braintree_gateway_spec.rb
@@ -0,0 +1,31 @@
+require 'rails_helper'
+require 'support/mock_data'
+
+RSpec.describe BraintreeGateway do
+ include_context 'mock_data'
+
+ before do
+ mock_requester = class_double(HTTParty)
+ @mock_response = double("Response")
+ allow(mock_requester).to receive(:post).and_return(@mock_response)
+
+ @gateway = BraintreeGateway.new(mock_requester)
+ end
+
+ describe "error handling" do
+ it "throws a GraphQLError for nil data" do
+ empty = {"data" => nil, "errors" => [{"message" => "an error message"}], "extensions" => {"requestId" => "not-a-real-request-1"}}
+ expect(@mock_response).to receive(:parsed_response).and_return(empty)
+
+ expect { @gateway.ping }.to raise_exception(BraintreeGateway::GraphQLError)
+ end
+
+ it "throws a GraphQLError when the expected data key is nil" do
+ empty = {"data" => {"ping" => nil}, "errors" => [{"message" => "another error message"}], "extensions" => {"requestId" => "not-a-real-request-2"}}
+ expect(@mock_response).to receive(:parsed_response).and_return(empty)
+
+ expect { @gateway.ping }.to raise_exception(BraintreeGateway::GraphQLError)
+ end
+ end
+
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index c03d738..e6a4dcc 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -41,8 +41,8 @@
end
config.before(:suite) do
- # Prevent Braintree from logging to standard out during tests
- Braintree::Configuration.logger = Logger.new("/dev/null")
+ # Prevent BraintreeGateway from logging to standard out during tests
+ BraintreeGateway::LOGGER = Logger.new("/dev/null")
end
# The settings below are suggested to provide a good initial experience
diff --git a/spec/support/mock_data.rb b/spec/support/mock_data.rb
index 910b73b..02cb43b 100644
--- a/spec/support/mock_data.rb
+++ b/spec/support/mock_data.rb
@@ -1,80 +1,137 @@
RSpec.shared_context 'mock_data' do
- let(:mock_transaction) {
- double(Braintree::Transaction,
- id: "my_id",
- type: "sale",
- amount: "10.0",
- status: "authorized",
- created_at: 1.minute.ago,
- updated_at: 1.minute.ago,
- credit_card_details: double(
- token: "ijkl",
- bin: "545454",
- last_4: "5454",
- card_type: "MasterCard",
- expiration_date: "12/2015",
- cardholder_name: "Bill Billson",
- customer_location: "US",
- ),
- customer_details: double(
- id: "h6hh3j",
- first_name: "Bill",
- last_name: "Billson",
- email: "bill@example.com",
- company: "Billy Bobby Pins",
- website: "bobby_pins.example.com",
- phone: "1234567890",
- fax: nil,
- ),
- )
+
+ let(:mock_successful_fetched_transaction) {
+ {
+ "data" => {
+ "transaction" => {
+ "id" => "my_id",
+ "amount" => {
+ "value" => "12.12",
+ "currencyIsoCode" => "CAD",
+ },
+ "status" => "SUBMITTED_FOR_SETTLEMENT",
+ "createdAt" => "2019-08-07T15:47:54.000000Z",
+ "paymentMethodSnapshot" => {
+ "__typename" => "CreditCardDetails",
+ "bin" => "545454",
+ "brandCode" => "MASTERCARD",
+ "cardholderName" => "Billy Bobby Pins",
+ "expirationMonth" => "12",
+ "expirationYear" => "2020",
+ "last4" => "4444",
+ "binData" => {
+ "countryOfIssuance" => "USA",
+ },
+ "origin" => nil,
+ },
+ }
+ },
+ "extensions" => {
+ "requestId" => "abc-request-123-id"
+ }
+ }
+ }
+
+ let(:mock_processor_decline_fetched_transaction) {
+ {
+ "data" => {
+ "transaction" => {
+ "id" => "spaceodyssey",
+ "amount" => {
+ "value" => "2001.00",
+ "currencyIsoCode" => "USD",
+ },
+ "status" => "PROCESSOR_DECLINED",
+ "gatewayRejectionReason" => nil,
+ "processorResponse" => {
+ "legacyCode" => "2001",
+ "message" => "Insufficient Funds",
+ },
+ "paymentMethodSnapshot" => {
+ "__typename" => "CreditCardDetails",
+ "bin" => "545454",
+ "brandCode" => "MASTERCARD",
+ "cardholderName" => "Billy Bobby Pins",
+ "expirationMonth" => "12",
+ "expirationYear" => "2020",
+ "last4" => "4444",
+ "binData" => {
+ "countryOfIssuance" => "USA",
+ },
+ "origin" => nil,
+ },
+ }
+ },
+ "extensions" => {
+ "requestId" => "def-request-456-id"
+ }
+ }
}
- let(:mock_failed_transaction) {
- double(Braintree::Transaction,
- id: "my_id",
- type: "sale",
- amount: "10.0",
- status: "processor_declined",
- created_at: 1.minute.ago,
- updated_at: 1.minute.ago,
- credit_card_details: double(
- token: "ijkl",
- bin: "545454",
- last_4: "5454",
- card_type: "MasterCard",
- expiration_date: "12/2015",
- cardholder_name: "Bill Billson",
- customer_location: "US",
- ),
- customer_details: double(
- id: "h6hh3j",
- first_name: "Bill",
- last_name: "Billson",
- email: "bill@example.com",
- company: "Billy Bobby Pins",
- website: "bobby_pins.example.com",
- phone: "1234567890",
- fax: nil,
- ),
- )
+ let(:mock_created_transaction) {
+ {
+ "data" => {
+ "chargePaymentMethod" => {
+ "transaction" => {
+ "id" => "my_id"
+ }
+ }
+ }
+ }
}
- let(:sale_error_result) {
- double(Braintree::ErrorResult,
- success?: false,
- message: "Amount is an invalid format. Unknown payment_method_nonce.",
- transaction: nil,
- errors: [
- OpenStruct.new(code: 81503, message: "Amount is an invalid format."),
- OpenStruct.new(code: 91565, message: "Unknown payment_method_nonce."),
- ]
- )
+ let(:mock_transaction_validation_error) {
+ {
+ "data" => {
+ "chargePaymentMethod" => nil,
+ },
+ "errors" => [
+ {
+ "message" => "Unknown or expired payment method ID.",
+ "locations" => [
+ {
+ "line" => 2,
+ "column" => 3
+ }
+ ],
+ "path" => [
+ "chargePaymentMethod"
+ ],
+ "extensions" => {
+ "errorType" => "user_error",
+ "errorClass" => "VALIDATION",
+ "legacyCode" => "91565",
+ "inputPath" => [
+ "input",
+ "transaction",
+ "singleUseTokenId"
+ ]
+ }
+ }
+ ],
+ "extensions" => {
+ "requestId" => "ghi-request-789-id"
+ }
+ }
}
- let(:processor_declined_result) {
- double(Braintree::ErrorResult,
- success?: false,
- transaction: OpenStruct.new(status: "processor_declined", id: "my_id"),
- )
+ let(:mock_transaction_graphql_error) {
+ {
+ "data" => nil,
+ "errors" => [
+ {
+ "message" => "Variable 'amount' has an invalid value. Values of type Amount must contain exactly 0, 2 or 3 decimal places.",
+ "locations" => [
+ {
+ "line" => 1,
+ "column" => 11
+ }
+ ]
+ }
+ ],
+ "extensions" => {
+ "requestId" => "jkl-request-012-id"
+ }
+ }
}
end
diff --git a/spec/views/checkouts/show.html.erb_spec.rb b/spec/views/checkouts/show.html.erb_spec.rb
index e03a3fe..f8f83fb 100644
--- a/spec/views/checkouts/show.html.erb_spec.rb
+++ b/spec/views/checkouts/show.html.erb_spec.rb
@@ -5,7 +5,7 @@
include_context 'mock_data'
before(:each) do
- assign(:transaction, mock_transaction)
+ assign(:transaction, mock_successful_fetched_transaction["data"]["transaction"])
end
it "renders the Transaction header" do