fidor_api 0.0.2 → 0.1.0

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 (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