arrow_payments 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|