fidor_api 0.0.2 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +5 -3
  5. data/CHANGELOG.md +114 -0
  6. data/Gemfile +3 -2
  7. data/README.md +12 -10
  8. data/lib/fidor_api/account.rb +7 -10
  9. data/lib/fidor_api/amount_attributes.rb +23 -20
  10. data/lib/fidor_api/approval_required.rb +9 -0
  11. data/lib/fidor_api/auth.rb +1 -1
  12. data/lib/fidor_api/beneficiary/ach.rb +39 -0
  13. data/lib/fidor_api/beneficiary/base.rb +118 -0
  14. data/lib/fidor_api/beneficiary/generic.rb +24 -0
  15. data/lib/fidor_api/beneficiary/p2p_account_number.rb +14 -0
  16. data/lib/fidor_api/beneficiary/p2p_phone.rb +14 -0
  17. data/lib/fidor_api/beneficiary/p2p_username.rb +14 -0
  18. data/lib/fidor_api/beneficiary/swift.rb +35 -0
  19. data/lib/fidor_api/beneficiary/unknown.rb +16 -0
  20. data/lib/fidor_api/beneficiary/utility.rb +35 -0
  21. data/lib/fidor_api/beneficiary.rb +27 -0
  22. data/lib/fidor_api/card.rb +31 -48
  23. data/lib/fidor_api/card_limit_attribute.rb +52 -0
  24. data/lib/fidor_api/card_limits.rb +6 -34
  25. data/lib/fidor_api/client.rb +8 -2
  26. data/lib/fidor_api/collection.rb +15 -6
  27. data/lib/fidor_api/confirmable_action.rb +45 -0
  28. data/lib/fidor_api/connectivity/connection.rb +113 -0
  29. data/lib/fidor_api/connectivity/endpoint.rb +82 -0
  30. data/lib/fidor_api/connectivity/resource.rb +76 -0
  31. data/lib/fidor_api/connectivity.rb +17 -0
  32. data/lib/fidor_api/constants.rb +3 -0
  33. data/lib/fidor_api/customer.rb +41 -23
  34. data/lib/fidor_api/customers/confirmations.rb +19 -0
  35. data/lib/fidor_api/errors.rb +14 -3
  36. data/lib/fidor_api/message.rb +17 -18
  37. data/lib/fidor_api/msisdn.rb +1 -1
  38. data/lib/fidor_api/password.rb +30 -0
  39. data/lib/fidor_api/preauth.rb +5 -12
  40. data/lib/fidor_api/session_token.rb +20 -0
  41. data/lib/fidor_api/transaction.rb +5 -12
  42. data/lib/fidor_api/transfer/ach.rb +46 -0
  43. data/lib/fidor_api/transfer/bank_internal.rb +37 -0
  44. data/lib/fidor_api/transfer/base.rb +36 -0
  45. data/lib/fidor_api/transfer/fps.rb +56 -0
  46. data/lib/fidor_api/transfer/generic.rb +134 -0
  47. data/lib/fidor_api/transfer/internal.rb +53 -0
  48. data/lib/fidor_api/transfer/p2p_account_number.rb +45 -0
  49. data/lib/fidor_api/transfer/p2p_phone.rb +45 -0
  50. data/lib/fidor_api/transfer/p2p_username.rb +45 -0
  51. data/lib/fidor_api/transfer/sepa.rb +56 -0
  52. data/lib/fidor_api/transfer/swift.rb +49 -0
  53. data/lib/fidor_api/transfer/utility.rb +50 -0
  54. data/lib/fidor_api/transfer.rb +13 -186
  55. data/lib/fidor_api/user.rb +12 -11
  56. data/lib/fidor_api/version.rb +1 -1
  57. data/lib/fidor_api.rb +39 -22
  58. metadata +37 -4
  59. data/lib/fidor_api/resource.rb +0 -84
@@ -1,8 +1,10 @@
1
1
  module FidorApi
2
-
3
- class Card < Resource
2
+ class Card < Connectivity::Resource
4
3
  extend ModelAttribute
5
4
  extend AmountAttributes
5
+ include CardLimitAttribute
6
+
7
+ self.endpoint = Connectivity::Endpoint.new('/cards', :collection)
6
8
 
7
9
  attribute :id, :integer
8
10
  attribute :account_id, :string
@@ -16,79 +18,60 @@ module FidorApi
16
18
  attribute :sms_notification, :boolean
17
19
  attribute :payed, :boolean
18
20
  attribute :state, :string
21
+ attribute :valid_until, :time
19
22
  attribute :lock_reason, :string
20
23
  attribute :disabled, :boolean
24
+ attribute :address, :json
21
25
  attribute :created_at, :time
22
26
  attribute :updated_at, :time
23
27
 
24
28
  amount_attribute :balance
25
- amount_attribute :atm_limit
26
- amount_attribute :transaction_single_limit
27
- amount_attribute :transaction_volume_limit
28
29
 
29
30
  def self.required_attributes
30
- [ :account_id, :type, :design, :currency, :pin ]
31
+ %i(account_id type)
31
32
  end
32
33
 
33
- validates *required_attributes, presence: true
34
-
35
- def self.all(access_token, options = {})
36
- Collection.build(self, request(access_token: access_token, endpoint: "/cards", query_params: options).body)
34
+ def self.writeable_attributes
35
+ required_attributes + %i(pin address)
37
36
  end
38
37
 
39
- def self.find(access_token, id)
40
- new(request(access_token: access_token, endpoint: "/cards/#{id}").body)
38
+ validates(*required_attributes, presence: true)
39
+
40
+ def activate
41
+ endpoint.for(self).put(action: 'activate')
42
+ true
41
43
  end
42
44
 
43
- def self.lock(access_token, id)
44
- request(method: :put, access_token: access_token, endpoint: "/cards/#{id}/lock")
45
+ def lock
46
+ endpoint.for(self).put(action: 'lock')
45
47
  true
46
48
  end
47
49
 
48
- def self.unlock(access_token, id)
49
- request(method: :put, access_token: access_token, endpoint: "/cards/#{id}/unlock")
50
+ def unlock
51
+ endpoint.for(self).put(action: 'unlock')
50
52
  true
51
53
  end
52
54
 
53
- def save
54
- if id.nil?
55
- create
55
+ def cancel(reason: 'lost')
56
+ case reason
57
+ when 'lost'
58
+ endpoint.for(self).put(action: 'cancel')
59
+ when 'stolen'
60
+ endpoint.for(self).put(action: 'block')
56
61
  else
57
- raise NoUpdatesAllowedError
62
+ fail ArgumentError, "Unknown reason: #{reason.inspect}"
58
63
  end
64
+ true
59
65
  end
60
66
 
61
67
  def as_json
62
- attributes.slice *self.class.required_attributes
68
+ attributes.slice(*self.class.writeable_attributes)
63
69
  end
64
70
 
65
- private
66
-
67
- def self.resource
68
- "cards"
69
- end
70
-
71
- module ClientSupport
72
- def cards(options = {})
73
- Card.all(token.access_token, options)
74
- end
75
-
76
- def card(id)
77
- Card.find(token.access_token, id)
78
- end
79
-
80
- def lock_card(id)
81
- Card.lock(token.access_token, id)
82
- end
83
-
84
- def unlock_card(id)
85
- Card.unlock(token.access_token, id)
86
- end
87
-
88
- def build_card(attributes = {})
89
- Card.new(attributes.merge(client: self))
90
- end
71
+ # comfort shorthands for easier validations
72
+ %w(name line_1 line_2 city postal_code country).each do |field|
73
+ define_method("address_#{field}") { address.try :[], field }
74
+ define_method("address_#{field}=") { |val| self.address ||= {}; address[field] = val }
91
75
  end
92
76
  end
93
-
94
77
  end
@@ -0,0 +1,52 @@
1
+ module FidorApi
2
+ module CardLimitAttribute
3
+ def self.included(base)
4
+ base.attribute :limits, :json
5
+ base.validate :validate_limits
6
+ end
7
+
8
+ def method_missing(symbol, *args)
9
+ if m = symbol.to_s.match(/(.*)_limit$/)
10
+ limits[m[1]]
11
+ elsif m = symbol.to_s.match(/(.*)_limit=$/)
12
+ write_limit(m[1], args[0])
13
+ else
14
+ super
15
+ end
16
+ end
17
+
18
+ def respond_to_missing?(symbol, include_all = false)
19
+ if symbol.to_s =~ /.*_limit=?$/
20
+ return true
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def write_limit(key, value)
29
+ self.limits ||= {}
30
+ # If the client is using BigDecimal, we will cast it to cents for him
31
+ if value.instance_of?(BigDecimal)
32
+ self.limits[key] = (value * 100.00).to_i
33
+ else
34
+ self.limits[key] = value
35
+ end
36
+ end
37
+
38
+ def validate_limits
39
+ limits.each do |key, value|
40
+ if value.instance_of?(BigDecimal)
41
+ limits[key] = (value * 100.00).to_i
42
+ elsif !(value.class.name.in?(INTEGER_CLASSES) || value.instance_of?(NilClass))
43
+ errors.add(:"#{key}_limit", :not_an_integer)
44
+ next
45
+ end
46
+ if limits[key] < 0
47
+ errors.add(:"#{key}_limit", :greater_than_or_equal_to, count: 0)
48
+ end
49
+ end if limits
50
+ end
51
+ end
52
+ end
@@ -1,42 +1,14 @@
1
1
  module FidorApi
2
-
3
- class CardLimits < Resource
2
+ class CardLimits < Connectivity::Resource
4
3
  extend ModelAttribute
5
- extend AmountAttributes
6
-
7
- attribute :id, :integer
8
- amount_attribute :atm_limit
9
- amount_attribute :transaction_single_limit
10
- amount_attribute :transaction_volume_limit
11
-
12
- def self.find(access_token, id)
13
- attributes = request(access_token: access_token, endpoint: "/cards/#{id}/limits").body
14
- attributes.merge!(id: id)
15
- new(attributes)
16
- end
4
+ include CardLimitAttribute
17
5
 
18
- def self.change(access_token, id, limits = {})
19
- attributes = limits.merge(id: id)
6
+ self.endpoint = Connectivity::Endpoint.new('/cards/:id/limits', :resource)
20
7
 
21
- new(attributes).tap do |record|
22
- record.set_attributes request(
23
- method: :put,
24
- access_token: access_token,
25
- endpoint: "/cards/#{id}/limits",
26
- body: record.as_json
27
- ).body
28
- end
29
- end
30
-
31
- module ClientSupport
32
- def card_limits(id)
33
- CardLimits.find(token.access_token, id)
34
- end
8
+ attribute :id, :integer
35
9
 
36
- def change_card_limits(id, limits)
37
- CardLimits.change(token.access_token, id, limits)
38
- end
10
+ def as_json
11
+ attributes.slice(:limits)
39
12
  end
40
13
  end
41
-
42
14
  end
@@ -6,15 +6,21 @@ module FidorApi
6
6
  attr_accessor :token
7
7
 
8
8
  include Account::ClientSupport
9
- include Card::ClientSupport
10
- include CardLimits::ClientSupport
9
+ include Beneficiary::ClientSupport
10
+ include Beneficiary::ACH::ClientSupport
11
+ include ConfirmableAction::ClientSupport
11
12
  include Customer::ClientSupport
12
13
  include Message::ClientSupport
13
14
  include Preauth::ClientSupport
15
+ include SessionToken::ClientSupport
14
16
  include Transaction::ClientSupport
17
+ include Transfer::ACH::ClientSupport
15
18
  include Transfer::Internal::ClientSupport
16
19
  include Transfer::SEPA::ClientSupport
17
20
  include Transfer::FPS::ClientSupport
21
+ include Transfer::P2pAccountNumber::ClientSupport
22
+ include Transfer::P2pPhone::ClientSupport
23
+ include Transfer::P2pUsername::ClientSupport
18
24
  include User::ClientSupport
19
25
  end
20
26
 
@@ -5,18 +5,27 @@ module FidorApi
5
5
  include Enumerable
6
6
 
7
7
  attr_accessor :records
8
- attr_accessor :total_pages, :current_page, :limit_value
8
+ attr_accessor :total_pages, :current_page, :limit_value, :total_entries
9
9
 
10
10
  def self.build(klass, response)
11
11
  new.tap do |object|
12
12
  data = response["data"]
13
13
  collection = response["collection"]
14
14
 
15
- object.records = data.map { |record| klass.new(record) }
15
+ object.records = data.map do |record|
16
+ class_to_instantiate = if block_given?
17
+ yield(record)
18
+ else
19
+ klass
20
+ end
16
21
 
17
- object.total_pages = collection["total_pages"]
18
- object.current_page = collection["current_page"]
19
- object.limit_value = collection["per_page"]
22
+ class_to_instantiate.new(record)
23
+ end
24
+
25
+ object.total_pages = collection["total_pages"]
26
+ object.current_page = collection["current_page"]
27
+ object.limit_value = collection["per_page"]
28
+ object.total_entries = collection["total_entries"]
20
29
  end
21
30
  end
22
31
 
@@ -34,4 +43,4 @@ module FidorApi
34
43
  end
35
44
  end
36
45
 
37
- end
46
+ end
@@ -0,0 +1,45 @@
1
+ module FidorApi
2
+ class ConfirmableAction < Connectivity::Resource
3
+ extend ModelAttribute
4
+
5
+ self.endpoint = Connectivity::Endpoint.new('/confirmable/actions', :collection)
6
+
7
+ attribute :id, :string
8
+ attribute :type, :string
9
+ attribute :message, :string
10
+ attribute :steps_left, :json
11
+ attribute :steps_completed, :json
12
+ attribute :resource, :json
13
+ attribute :succeeded_at, :time
14
+ attribute :failed_at, :time
15
+ attribute :errored_at, :time
16
+ attribute :created_at, :time
17
+ attribute :updated_at, :time
18
+
19
+ attribute :otp, :string
20
+ attribute :approval, :string
21
+
22
+ def refresh
23
+ endpoint.for(self).put(action: "refresh")
24
+ true
25
+ end
26
+
27
+ module ClientSupport
28
+ def confirmable_actions(options = {})
29
+ ConfirmableAction.all
30
+ end
31
+
32
+ def confirmable_action(id)
33
+ ConfirmableAction.find(id)
34
+ end
35
+
36
+ def update_confirmable_action(id, attributes)
37
+ ConfirmableAction.new(attributes.merge(id: id)).save
38
+ end
39
+
40
+ def refresh_confirmable_action(id)
41
+ ConfirmableAction.new(id: id).refresh
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,113 @@
1
+ module FidorApi
2
+ module Connectivity
3
+ module Connection
4
+ extend self
5
+
6
+ Response = Struct.new(:status, :headers, :raw_body) do
7
+ def body
8
+ if headers["content-type"] =~ /json/
9
+ JSON.parse(raw_body)
10
+ else
11
+ raw_body
12
+ end
13
+ end
14
+ end
15
+
16
+ def get(path, options={})
17
+ request(:get, path, options)
18
+ end
19
+
20
+ def post(path, options={})
21
+ request(:post, path, options)
22
+ end
23
+
24
+ def put(path, options={})
25
+ request(:put, path, options)
26
+ end
27
+
28
+ def delete(path, options={})
29
+ request(:delete, path, options)
30
+ end
31
+
32
+ def with_token(token)
33
+ self.access_token = token
34
+ yield
35
+ end
36
+
37
+ private
38
+
39
+ def request(method, path, options={})
40
+ options.reverse_merge! version: 1, access_token: Connectivity.access_token
41
+ response = faraday.public_send(method, [FidorApi.configuration.api_path, path].compact.join) do |request|
42
+ request.params = options[:query_params] if options[:query_params]
43
+ request.headers = {}
44
+ if options[:access_token]
45
+ request.headers["Authorization"] = "Bearer #{options[:access_token]}"
46
+ else
47
+ request.headers["Authorization"] = tokenless_http_basic_header
48
+ end
49
+ request.headers["Accept"] = "application/vnd.fidor.de; version=#{options[:version]},text/json"
50
+ request.headers["Content-Type"] = "application/json"
51
+ if options[:body]
52
+ if options[:body].is_a?(String)
53
+ request.body = options[:body]
54
+ elsif options[:body].respond_to?(:to_json)
55
+ request.body = options[:body].to_json
56
+ else
57
+ fail ArgumentError, "unhandled body type #{options[:body].inspect}"
58
+ end
59
+ end
60
+ end
61
+ if response.status == 303 && URI.parse(response.headers["Location"]).path =~ /^(\/fidor_api)?\/confirmable\//
62
+ confirmable_action = ConfirmableAction.new(id: URI.parse(response.headers["Location"]).path.split("/").last)
63
+ raise ApprovalRequired.new(confirmable_action)
64
+ end
65
+ Response.new(response.status, response.headers, response.body)
66
+ rescue Faraday::Error::ClientError => e
67
+ log :info, "Error (#{e.class.name}): #{e.to_s}\nStatus: #{e.response[:status]}"
68
+ log :debug, "Header: #{e.response[:header]}\nBody: #{e.response[:body]}" if e.response[:status] != 500
69
+ case e.response[:status]
70
+ when 401
71
+ raise UnauthorizedTokenError
72
+ when 403
73
+ body = JSON.parse(e.response[:body])
74
+ raise ForbiddenError.new(body["message"], body["code"], body["key"])
75
+ when 422
76
+ body = JSON.parse(e.response[:body])
77
+ raise ValidationError.new(body["message"], body["errors"], body["key"])
78
+ else
79
+ raise ClientError.new(e.response[:body])
80
+ end
81
+ end
82
+
83
+ def log(level, message)
84
+ return unless FidorApi.configuration.logging
85
+ FidorApi.configuration.logger.public_send(level, message)
86
+ end
87
+
88
+ def logger_type
89
+ if defined?(Faraday::DetailedLogger)
90
+ :detailed_logger
91
+ else
92
+ :logger
93
+ end
94
+ end
95
+
96
+ def tokenless_http_basic_header
97
+ @tokenless_http_basic_header ||= begin
98
+ base64 = Base64.strict_encode64("#{FidorApi.configuration.htauth_user}:#{FidorApi.configuration.htauth_password}")
99
+ "Basic #{base64}"
100
+ end
101
+ end
102
+
103
+ def faraday
104
+ @faraday ||= Faraday.new(url: FidorApi.configuration.api_url, ssl: { verify: FidorApi.configuration.verify_ssl }) do |config|
105
+ config.request :url_encoded
106
+ config.response logger_type, FidorApi.configuration.logger if FidorApi.configuration.logging
107
+ config.response :raise_error
108
+ config.adapter Faraday.default_adapter
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,82 @@
1
+ module FidorApi
2
+ module Connectivity
3
+ class Endpoint
4
+ attr_reader :collection, :resource, :version, :tokenless
5
+
6
+ def initialize(path, mode, version: '1', tokenless: false)
7
+ @path = path
8
+ @version = version
9
+ @tokenless = tokenless
10
+
11
+ case mode
12
+ when :collection
13
+ @collection = path
14
+ @resource = "#{path}/:id"
15
+ when :resource
16
+ @resource = path
17
+ else
18
+ fail ArgumentError, "mode #{mode.inspect} must be resource or collection"
19
+ end
20
+ end
21
+
22
+ class Context
23
+ def initialize(endpoint, object)
24
+ @endpoint = endpoint
25
+ @object = object
26
+ end
27
+
28
+ def get(target: :resource, action: nil, query_params: nil, tokenless: nil)
29
+ request :get, target, action, query_params: query_params, tokenless: tokenless
30
+ end
31
+
32
+ def post(target: :collection, action: nil, payload: nil, tokenless: nil)
33
+ request :post, target, action, body: payload, tokenless: tokenless
34
+ end
35
+
36
+ def put(target: :resource, action: nil, payload: nil, tokenless: nil)
37
+ request :put, target, action, body: payload, tokenless: tokenless
38
+ end
39
+
40
+ def delete(target: :resource, action: nil, tokenless: nil)
41
+ request :delete, target, action, tokenless: tokenless
42
+ end
43
+
44
+ private
45
+
46
+ def request(method, target, action, options = {})
47
+ options.reverse_merge! version: @endpoint.version
48
+ options[:access_token] = nil if options[:tokenless] || @endpoint.tokenless
49
+ Connection.public_send(method, send("#{target}_path", action), options)
50
+ end
51
+
52
+ def resource_path(action = nil)
53
+ interpolate(@endpoint.resource, action)
54
+ end
55
+
56
+ def collection_path(action = nil)
57
+ interpolate(@endpoint.collection, action)
58
+ end
59
+
60
+ def interpolate(path, suffix = nil)
61
+ [path, suffix].compact.join('/').gsub(/:(\w+)/) do |m|
62
+ fetch_option $1
63
+ end
64
+ end
65
+
66
+ def fetch_option(name)
67
+ if @object.kind_of? Hash
68
+ @object[name]
69
+ elsif @object.class.name.in?(INTEGER_CLASSES) || @object.kind_of?(String)
70
+ @object
71
+ elsif @object.respond_to? name
72
+ @object.public_send name
73
+ end
74
+ end
75
+ end
76
+
77
+ def for(object)
78
+ Context.new(self, object)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,76 @@
1
+ module FidorApi
2
+ module Connectivity
3
+ class Resource
4
+ include ActiveModel::Model
5
+ extend ModelAttribute
6
+
7
+ class_attribute :endpoint
8
+
9
+ attr_accessor :error_keys
10
+
11
+ class << self
12
+ def find(id)
13
+ new endpoint.for(id).get.body.reverse_merge(id: id)
14
+ end
15
+
16
+ def all(options = {})
17
+ FidorApi::Collection.build self, endpoint.for(self).get(target: :collection, query_params: options).body
18
+ end
19
+
20
+ def model_name
21
+ ActiveModel::Name.new(self, nil, self.name.sub("FidorApi::", ""))
22
+ end
23
+ end
24
+
25
+ def initialize(attributes = {})
26
+ set_attributes attributes
27
+ end
28
+
29
+ def reload
30
+ set_attributes endpoint.for(self).get.body
31
+ end
32
+
33
+ def persisted?
34
+ id.present?
35
+ end
36
+
37
+ def save
38
+ if valid?
39
+ set_attributes(persisted? ? remote_update.body : remote_create.body)
40
+ true
41
+ else
42
+ false
43
+ end
44
+ rescue ValidationError => e
45
+ self.error_keys = e.error_keys
46
+ map_errors(e.fields)
47
+ false
48
+ end
49
+
50
+ def update_attributes(attributes={})
51
+ set_attributes attributes
52
+ valid? and remote_update attributes.keys
53
+ end
54
+
55
+ private
56
+
57
+ def remote_create
58
+ endpoint.for(self).post(payload: self.as_json)
59
+ end
60
+
61
+ def remote_update(*attributes)
62
+ payload = self.as_json.with_indifferent_access
63
+ payload.slice!(*attributes.flatten) if attributes.present?
64
+ endpoint.for(self).put(payload: payload)
65
+ end
66
+
67
+ def map_errors(fields)
68
+ fields.each do |hash|
69
+ hash.symbolize_keys!
70
+ field = hash[:field].to_sym
71
+ errors.add(field, hash[:message], hash) if respond_to?(field) || field == :base
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,17 @@
1
+ module FidorApi
2
+ module Connectivity
3
+ extend self
4
+
5
+ autoload :Connection, 'fidor_api/connectivity/connection'
6
+ autoload :Resource, 'fidor_api/connectivity/resource'
7
+ autoload :Endpoint, 'fidor_api/connectivity/endpoint'
8
+
9
+ def access_token=(val)
10
+ Thread.current[:fidor_api_access_token] = val
11
+ end
12
+
13
+ def access_token
14
+ Thread.current[:fidor_api_access_token]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module FidorApi
2
+ INTEGER_CLASSES = %w(Integer Fixnum Bignum)
3
+ end