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.
Files changed (46) hide show
  1. data/.gitignore +25 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +3 -0
  5. data/README.md +272 -0
  6. data/Rakefile +10 -0
  7. data/arrow_payments.gemspec +25 -0
  8. data/lib/arrow_payments.rb +25 -0
  9. data/lib/arrow_payments/address.rb +11 -0
  10. data/lib/arrow_payments/client.rb +57 -0
  11. data/lib/arrow_payments/client/customers.rb +45 -0
  12. data/lib/arrow_payments/client/payment_methods.rb +90 -0
  13. data/lib/arrow_payments/client/transactions.rb +64 -0
  14. data/lib/arrow_payments/configuration.rb +7 -0
  15. data/lib/arrow_payments/connection.rb +78 -0
  16. data/lib/arrow_payments/customer.rb +38 -0
  17. data/lib/arrow_payments/entity.rb +39 -0
  18. data/lib/arrow_payments/errors.rb +13 -0
  19. data/lib/arrow_payments/line_item.rb +10 -0
  20. data/lib/arrow_payments/payment_method.rb +19 -0
  21. data/lib/arrow_payments/recurring_billing.rb +30 -0
  22. data/lib/arrow_payments/transaction.rb +74 -0
  23. data/lib/arrow_payments/version.rb +3 -0
  24. data/spec/arrow_payments_spec.rb +40 -0
  25. data/spec/client_spec.rb +42 -0
  26. data/spec/configuration_spec.rb +24 -0
  27. data/spec/customer_spec.rb +29 -0
  28. data/spec/customers_spec.rb +160 -0
  29. data/spec/entity_spec.rb +55 -0
  30. data/spec/fixtures/complete_payment_method.json +37 -0
  31. data/spec/fixtures/customer.json +25 -0
  32. data/spec/fixtures/customers.json +133 -0
  33. data/spec/fixtures/headers/payment_method_complete.yml +9 -0
  34. data/spec/fixtures/headers/payment_method_start.yml +9 -0
  35. data/spec/fixtures/line_item.json +8 -0
  36. data/spec/fixtures/start_payment_method.json +6 -0
  37. data/spec/fixtures/transaction.json +29 -0
  38. data/spec/fixtures/transaction_capture.json +5 -0
  39. data/spec/fixtures/transactions.json +68 -0
  40. data/spec/line_item_spec.rb +17 -0
  41. data/spec/payment_method_spec.rb +16 -0
  42. data/spec/payment_methods_spec.rb +181 -0
  43. data/spec/recurring_billing_spec.rb +28 -0
  44. data/spec/spec_helper.rb +19 -0
  45. data/spec/transactions_spec.rb +101 -0
  46. 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,7 @@
1
+ module ArrowPayments
2
+ class Configuration
3
+ class << self
4
+ attr_accessor :api_key, :mode, :merchant_id
5
+ end
6
+ end
7
+ 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