arrow_payments 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +25 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +3 -0
- data/README.md +272 -0
- data/Rakefile +10 -0
- data/arrow_payments.gemspec +25 -0
- data/lib/arrow_payments.rb +25 -0
- data/lib/arrow_payments/address.rb +11 -0
- data/lib/arrow_payments/client.rb +57 -0
- data/lib/arrow_payments/client/customers.rb +45 -0
- data/lib/arrow_payments/client/payment_methods.rb +90 -0
- data/lib/arrow_payments/client/transactions.rb +64 -0
- data/lib/arrow_payments/configuration.rb +7 -0
- data/lib/arrow_payments/connection.rb +78 -0
- data/lib/arrow_payments/customer.rb +38 -0
- data/lib/arrow_payments/entity.rb +39 -0
- data/lib/arrow_payments/errors.rb +13 -0
- data/lib/arrow_payments/line_item.rb +10 -0
- data/lib/arrow_payments/payment_method.rb +19 -0
- data/lib/arrow_payments/recurring_billing.rb +30 -0
- data/lib/arrow_payments/transaction.rb +74 -0
- data/lib/arrow_payments/version.rb +3 -0
- data/spec/arrow_payments_spec.rb +40 -0
- data/spec/client_spec.rb +42 -0
- data/spec/configuration_spec.rb +24 -0
- data/spec/customer_spec.rb +29 -0
- data/spec/customers_spec.rb +160 -0
- data/spec/entity_spec.rb +55 -0
- data/spec/fixtures/complete_payment_method.json +37 -0
- data/spec/fixtures/customer.json +25 -0
- data/spec/fixtures/customers.json +133 -0
- data/spec/fixtures/headers/payment_method_complete.yml +9 -0
- data/spec/fixtures/headers/payment_method_start.yml +9 -0
- data/spec/fixtures/line_item.json +8 -0
- data/spec/fixtures/start_payment_method.json +6 -0
- data/spec/fixtures/transaction.json +29 -0
- data/spec/fixtures/transaction_capture.json +5 -0
- data/spec/fixtures/transactions.json +68 -0
- data/spec/line_item_spec.rb +17 -0
- data/spec/payment_method_spec.rb +16 -0
- data/spec/payment_methods_spec.rb +181 -0
- data/spec/recurring_billing_spec.rb +28 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/transactions_spec.rb +101 -0
- metadata +225 -0
@@ -0,0 +1,45 @@
|
|
1
|
+
module ArrowPayments
|
2
|
+
module Customers
|
3
|
+
# Get all existing customers
|
4
|
+
# @return [Array<Customer>]
|
5
|
+
def customers
|
6
|
+
get('/customers').map { |c| Customer.new(c) }
|
7
|
+
end
|
8
|
+
|
9
|
+
# Get an existing customer
|
10
|
+
# @param [Integer] customer ID
|
11
|
+
# @return [Customer]
|
12
|
+
def customer(id)
|
13
|
+
Customer.new(get("/customer/#{id}"))
|
14
|
+
rescue NotFound
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
# Create a new customer
|
19
|
+
# @param [Hash] customer attributes
|
20
|
+
# @return [Customer]
|
21
|
+
def create_customer(options={})
|
22
|
+
customer = options.kind_of?(Hash) ? Customer.new(options) : options
|
23
|
+
Customer.new(post("/customer/add", customer.to_source_hash))
|
24
|
+
end
|
25
|
+
|
26
|
+
# Update an existing customer attributes
|
27
|
+
# @param [Customer] customer instance
|
28
|
+
# @return [Boolean] update result
|
29
|
+
def update_customer(customer)
|
30
|
+
params = customer.to_source_hash
|
31
|
+
params['CustomerID'] = customer.id
|
32
|
+
|
33
|
+
resp = post('/customer/update', params)
|
34
|
+
resp['Success'] == true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Delete an existing customer
|
38
|
+
# @param [Integer] customer ID
|
39
|
+
# @return [Boolean]
|
40
|
+
def delete_customer(id)
|
41
|
+
resp = post('/customer/delete', 'CustomerID' => id)
|
42
|
+
resp['Success'] == true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module ArrowPayments
|
2
|
+
module PaymentMethods
|
3
|
+
# Get a single payment method
|
4
|
+
# @param [Integer] customer ID
|
5
|
+
# @param [Integer] payment method ID
|
6
|
+
# @return [PaymentMethod] payment method instance
|
7
|
+
def payment_method(customer_id, id)
|
8
|
+
customer(customer_id).payment_methods.select { |cc| cc.id == id }.first
|
9
|
+
end
|
10
|
+
|
11
|
+
# Start a new payment method
|
12
|
+
# @param [Integer] customer ID
|
13
|
+
# @param [Address] billing address instance
|
14
|
+
# @param [String] return url
|
15
|
+
# @return [String] payment method form url
|
16
|
+
def start_payment_method(customer_id, billing_address, return_url=nil)
|
17
|
+
if billing_address.kind_of?(Hash)
|
18
|
+
billing_address = ArrowPayments::Address.new(billing_address)
|
19
|
+
end
|
20
|
+
|
21
|
+
params = {
|
22
|
+
'CustomerId' => customer_id,
|
23
|
+
'BillingAddress' => billing_address.to_source_hash
|
24
|
+
}
|
25
|
+
|
26
|
+
# If return url is blank means that its not browser-less payment method
|
27
|
+
# creation. Reponse should include token ID for the Step 3.
|
28
|
+
if return_url
|
29
|
+
params['ReturnUrl'] = return_url
|
30
|
+
end
|
31
|
+
|
32
|
+
post("/paymentmethod/start", params)['FormPostUrl']
|
33
|
+
end
|
34
|
+
|
35
|
+
# Setup a new payment method
|
36
|
+
# @param [String] payment method form url
|
37
|
+
# @param [PaymentMethod] payment method instance or hash
|
38
|
+
# @return [String] confirmation token
|
39
|
+
def setup_payment_method(form_url, payment_method)
|
40
|
+
cc = payment_method
|
41
|
+
|
42
|
+
if payment_method.kind_of?(Hash)
|
43
|
+
cc = ArrowPayments::PaymentMethod.new(payment_method)
|
44
|
+
end
|
45
|
+
|
46
|
+
resp = post_to_url(form_url, payment_method_form(cc))
|
47
|
+
resp.headers['location'].scan(/token-id=(.*)/).flatten.first
|
48
|
+
end
|
49
|
+
|
50
|
+
# Complete payment method creation
|
51
|
+
# @param [String] token ID
|
52
|
+
# @return [PaymentMethod]
|
53
|
+
def complete_payment_method(token_id)
|
54
|
+
resp = post('/paymentmethod/complete', 'TokenID' => token_id)
|
55
|
+
ArrowPayments::PaymentMethod.new(resp)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Create a new payment method. This is a wrapper on top of 3 step process
|
59
|
+
# @param [Integer] customer ID
|
60
|
+
# @param [Address] credit card address
|
61
|
+
# @param [PaymentMethod] credit card
|
62
|
+
# @return [PaymentMethod]
|
63
|
+
def create_payment_method(customer_id, address, card)
|
64
|
+
url = start_payment_method(customer_id, address)
|
65
|
+
token = setup_payment_method(url, card)
|
66
|
+
|
67
|
+
complete_payment_method(token)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Delete an existing payment method
|
71
|
+
# @param [Integer] payment method ID
|
72
|
+
# @return [Boolean]
|
73
|
+
def delete_payment_method(id)
|
74
|
+
resp = post('/paymentmethod/delete', 'PaymentMethodId' => id)
|
75
|
+
resp['Success'] == true
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def payment_method_form(cc)
|
81
|
+
{
|
82
|
+
'billing-cc-number' => cc.number,
|
83
|
+
'billing-cc-exp' => [cc.expiration_month, cc.expiration_year].join,
|
84
|
+
'billing-cvv' => cc.security_code,
|
85
|
+
'billing-first-name' => cc.first_name,
|
86
|
+
'billing-last-name' => cc.last_name
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ArrowPayments
|
2
|
+
module Transactions
|
3
|
+
# Get transaction details
|
4
|
+
# @param [String] transaction ID
|
5
|
+
# @return [Transaction] transactions instance
|
6
|
+
def transaction(id)
|
7
|
+
Transaction.new(get("/transaction/#{id}"))
|
8
|
+
rescue NotFound
|
9
|
+
nil
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get customer transactions by status
|
13
|
+
# @param [String] customer ID
|
14
|
+
# @param [String] transaction status
|
15
|
+
# @return [Array<Transaction>]
|
16
|
+
def transactions(customer_id, status='NotSettled')
|
17
|
+
unless Transaction::STATUSES.include?(status)
|
18
|
+
raise ArgumentError, "Invalid status: #{status}"
|
19
|
+
end
|
20
|
+
|
21
|
+
resp = get("/customer/#{customer_id}/Transactions/#{status}")
|
22
|
+
resp['Transactions'].map { |t| Transaction.new(t) }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create a new transaction
|
26
|
+
# @return [Transaction]
|
27
|
+
def create_transaction(transaction)
|
28
|
+
if transaction.kind_of?(Hash)
|
29
|
+
transaction = ArrowPayments::Transaction.new(transaction)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Set default transaction attributes
|
33
|
+
transaction.transaction_source = 'API'
|
34
|
+
|
35
|
+
params = transaction.to_source_hash
|
36
|
+
params['Amount'] = params['TotalAmount']
|
37
|
+
|
38
|
+
resp = post('/transaction/add', params)
|
39
|
+
|
40
|
+
if resp['Success'] == true
|
41
|
+
ArrowPayments::Transaction.new(resp)
|
42
|
+
else
|
43
|
+
raise ArrowPayments::Error, resp['Message']
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Capture an unsettled transaction
|
48
|
+
# @param [String] transaction ID
|
49
|
+
# @param [Integer] amount, less than or equal to original amount
|
50
|
+
# @return [Boolean]
|
51
|
+
def capture_transaction(id, amount)
|
52
|
+
resp = post('/transaction/capture', 'TransactionId' => id, 'Amount' => amount)
|
53
|
+
resp['Success'] == true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Void a previously submitted transaction that have not yet settled
|
57
|
+
# @param [String] transaction ID
|
58
|
+
# @return [Boolean]
|
59
|
+
def void_transaction(id)
|
60
|
+
resp = post('/transaction/void', 'TransactionId' => id)
|
61
|
+
resp['Success'] == true
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module ArrowPayments
|
5
|
+
module Connection
|
6
|
+
API_PRODUCTION = 'https://gateway.arrowpayments.com'
|
7
|
+
API_SANDBOX = 'http://demo.arrowpayments.com'
|
8
|
+
|
9
|
+
def get(path, params={}, raw=false)
|
10
|
+
request(:get, path, params, raw)
|
11
|
+
end
|
12
|
+
|
13
|
+
def post(path, params={}, raw=false)
|
14
|
+
request(:post, path, params, raw)
|
15
|
+
end
|
16
|
+
|
17
|
+
def post_to_url(url, params)
|
18
|
+
Faraday.post(url, params)
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def request(method, path, params={}, raw=false)
|
24
|
+
if method == :post
|
25
|
+
path = "/api#{path}"
|
26
|
+
|
27
|
+
params['ApiKey'] = api_key
|
28
|
+
params['MID'] = merchant_id
|
29
|
+
else
|
30
|
+
path = "/api/#{api_key}#{path}"
|
31
|
+
end
|
32
|
+
|
33
|
+
headers = {
|
34
|
+
'Accept' => 'application/json',
|
35
|
+
'Content-Type' => 'application/json'
|
36
|
+
}
|
37
|
+
|
38
|
+
api_url = production? ? API_PRODUCTION : API_SANDBOX
|
39
|
+
|
40
|
+
response = connection(api_url).send(method, path, params) do |request|
|
41
|
+
request.headers = headers
|
42
|
+
|
43
|
+
case method
|
44
|
+
when :get, :delete
|
45
|
+
request.url(path, params)
|
46
|
+
when :post, :put
|
47
|
+
request.path = path
|
48
|
+
request.body = params.to_json
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
unless response.success?
|
53
|
+
handle_failed_response(response)
|
54
|
+
end
|
55
|
+
|
56
|
+
raw ? response : JSON.parse(response.body)
|
57
|
+
end
|
58
|
+
|
59
|
+
def connection(url)
|
60
|
+
connection = Faraday.new(url) do |c|
|
61
|
+
c.use(Faraday::Request::UrlEncoded)
|
62
|
+
c.use(Faraday::Response::Logger) if debug?
|
63
|
+
c.adapter(Faraday.default_adapter)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def handle_failed_response(response)
|
68
|
+
case response.status
|
69
|
+
when 400
|
70
|
+
raise ArrowPayments::BadRequest, response.headers['error']
|
71
|
+
when 404
|
72
|
+
raise ArrowPayments::NotFound, response.headers['error']
|
73
|
+
when 500
|
74
|
+
raise ArrowPayments::Error, response.headers['error']
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ArrowPayments
|
2
|
+
class Customer < Entity
|
3
|
+
property :id, :from => 'ID'
|
4
|
+
property :name, :from => 'Name'
|
5
|
+
property :code, :from => 'Code'
|
6
|
+
property :contact, :from => 'PrimaryContact'
|
7
|
+
property :phone, :from => 'PrimaryContactPhone'
|
8
|
+
property :email, :from => 'PrimaryContactEmailAddress'
|
9
|
+
property :recurring_billings, :from => 'RecurrentBilling'
|
10
|
+
property :payment_methods, :from => 'PaymentMethods'
|
11
|
+
|
12
|
+
def PaymentMethods=(data)
|
13
|
+
if data.kind_of?(Array)
|
14
|
+
self.payment_methods = data.map { |d| PaymentMethod.new(d) }
|
15
|
+
else
|
16
|
+
self.payment_methods = []
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def PaymentMethods
|
21
|
+
(payment_methods || []).map(&:to_source_hash)
|
22
|
+
end
|
23
|
+
|
24
|
+
def RecurrentBillings=(data)
|
25
|
+
if data.kind_of?(Array)
|
26
|
+
self.recurring_billings = data.map { |d| RecurringBilling.new(d) }
|
27
|
+
else
|
28
|
+
self.recurring_billings = []
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_source_hash(options={})
|
33
|
+
hash = super(options)
|
34
|
+
hash.merge!('PaymentMethods' => (payment_methods || []).map(&:to_source_hash))
|
35
|
+
hash
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
|
3
|
+
module ArrowPayments
|
4
|
+
class Entity < Hashie::Trash
|
5
|
+
include Hashie::Extensions::Coercion
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_reader :properties_map
|
9
|
+
end
|
10
|
+
|
11
|
+
instance_variable_set('@properties_map', {})
|
12
|
+
|
13
|
+
def to_source_hash(options={})
|
14
|
+
hash = {}
|
15
|
+
|
16
|
+
self.class.properties_map.each_pair do |k, v|
|
17
|
+
val = send(k)
|
18
|
+
hash[v] = val unless val.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
hash
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.property(property_name, options = {})
|
25
|
+
super(property_name, options)
|
26
|
+
|
27
|
+
if options[:from]
|
28
|
+
@properties_map ||= {}
|
29
|
+
@properties_map[property_name.to_sym] = options[:from]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def property_exists?(property)
|
36
|
+
self.class.property?(property.to_sym)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ArrowPayments
|
2
|
+
# Generic API error
|
3
|
+
class Error < StandardError ; end
|
4
|
+
|
5
|
+
# Error when API entity is not found
|
6
|
+
class NotFound < Error ; end
|
7
|
+
|
8
|
+
# Error when API endpoint or method is not implemented
|
9
|
+
class NotImplemented < Error ; end
|
10
|
+
|
11
|
+
# Bad request error, does not contain a message
|
12
|
+
class BadRequest < Error ; end
|
13
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module ArrowPayments
|
2
|
+
class LineItem < Entity
|
3
|
+
property :id, :from => 'ID'
|
4
|
+
property :commodity_code, :from => 'CommodityCode'
|
5
|
+
property :description, :from => 'Description'
|
6
|
+
property :price, :from => 'Price'
|
7
|
+
property :product_code, :from => 'ProductCode'
|
8
|
+
property :unit_of_measure, :from => 'UnitOfMeasure'
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module ArrowPayments
|
2
|
+
class PaymentMethod < Entity
|
3
|
+
property :id, :from => 'ID'
|
4
|
+
property :card_type, :from => 'CardType'
|
5
|
+
property :last_digits, :from => 'Last4'
|
6
|
+
property :first_name, :from => 'CardholderFirstName'
|
7
|
+
property :last_name, :from => 'CardholderLastName'
|
8
|
+
property :expiration_month, :from => 'ExpirationMonth'
|
9
|
+
property :expiration_year, :from => 'ExpirationYear'
|
10
|
+
property :address, :from => 'BillingStreet1'
|
11
|
+
property :address2, :from => 'BillingStreet2'
|
12
|
+
property :city, :from => 'BillingCity'
|
13
|
+
property :state, :from => 'BillingState'
|
14
|
+
property :zip, :from => 'BillingZip'
|
15
|
+
|
16
|
+
property :number
|
17
|
+
property :security_code
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# From documentation:
|
2
|
+
# * Frequency -> W, M, Q, Y (weekly, monthly, quarterly, yearly)
|
3
|
+
# * TransactionDay -> Weekly: [1-7], Monthly: [1-28], Quarterly:[1,2,3], Yearly: [1-12]
|
4
|
+
|
5
|
+
module ArrowPayments
|
6
|
+
class RecurringBilling < Entity
|
7
|
+
FREQUENCIES = %w(W M Q Y)
|
8
|
+
FREQUENCY_NAMES = {
|
9
|
+
'W' => 'Weekly',
|
10
|
+
'M' => 'Monthly',
|
11
|
+
'Q' => 'Quarterly',
|
12
|
+
'Y' => 'Yearly'
|
13
|
+
}
|
14
|
+
|
15
|
+
property :id, :from => 'ID'
|
16
|
+
property :payment_method_id, :from => 'PaymentMethodId'
|
17
|
+
property :frequency, :from => 'Frequency'
|
18
|
+
property :total_amount, :from => 'TotalAmount'
|
19
|
+
property :shipping_amount, :from => 'ShippingAmount'
|
20
|
+
property :description, :from => 'Description'
|
21
|
+
property :transaction_day, :from => 'TransactionDay'
|
22
|
+
property :date_created, :from => 'DateCreated'
|
23
|
+
|
24
|
+
# Get billing frequency name
|
25
|
+
# @return [String]
|
26
|
+
def frequency_name
|
27
|
+
FREQUENCY_NAMES[frequency]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|