charging-client 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ # encoding: utf-8
2
+
3
+ module Charging
4
+ module Helpers
5
+ module_function
6
+
7
+ def load_variables(object, attributes, hash)
8
+ attributes.each do |attribute|
9
+ value = hash.fetch(attribute, hash.fetch(attribute.to_s, nil))
10
+ object.instance_variable_set "@#{attribute}", value
11
+ end
12
+ end
13
+
14
+ def required_arguments!(arguments)
15
+ errors = []
16
+
17
+ arguments.each do |key, value|
18
+ errors << "#{key} required" if value.nil?
19
+ end
20
+
21
+ raise ArgumentError, errors.join(', ') if errors.any?
22
+ end
23
+
24
+ def hashify(object, attributes)
25
+ attributes.inject({}) do |result, attribute|
26
+ result[attribute] = object.send(attribute)
27
+ result
28
+ end
29
+ end
30
+
31
+ def extract_uuid(uri)
32
+ uri.split("/").last
33
+ rescue
34
+ ""
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,96 @@
1
+ # encoding: utf-8
2
+ require 'base64'
3
+
4
+ module Charging
5
+ module Http # :nodoc:
6
+ class LastResponseError < RuntimeError
7
+ attr_reader :last_response
8
+
9
+ def initialize(last_response)
10
+ super
11
+ @last_response = last_response
12
+ end
13
+
14
+ def message
15
+ last_response.to_s
16
+ end
17
+ end
18
+
19
+ module_function
20
+
21
+ def get(path, token, params = {})
22
+ request_to_api(:get, path, params, token)
23
+ end
24
+
25
+ def delete(path, token, etag)
26
+ request_to_api(:delete, path, {etag: etag}, token)
27
+ end
28
+
29
+ def post(path, token, body = {}, params = {})
30
+ request_to_api(:post, path, params, token, body)
31
+ end
32
+
33
+ def put(path, token, etag, body = {})
34
+ request_to_api(:put, path, {etag: etag}, token, body)
35
+ end
36
+
37
+ def patch(path, token, etag, body = {})
38
+ request_to_api(:patch, path, {etag: etag}, token, body)
39
+ end
40
+
41
+ def basic_credential_for(user, password = nil)
42
+ credential_for = user.to_s
43
+ credential_for << ":#{password}" unless password.nil?
44
+
45
+ credential = ::Base64.strict_encode64(credential_for)
46
+ "Basic #{credential}"
47
+ end
48
+
49
+ def should_follow_redirect(follow = true)
50
+ proc { |response, request, result, &block|
51
+ if follow && [301, 302, 307].include?(response.code)
52
+ response.follow_redirection(request, result, &block)
53
+ else
54
+ response.return!(request, result, &block)
55
+ end
56
+ }
57
+ end
58
+
59
+ def request_to_api(method, path, params, token, body = nil)
60
+ path = charging_path(path) unless path.start_with?('http')
61
+ etag = params.delete(:etag)
62
+
63
+ args = [method, path]
64
+ args << encoded_body(body) if body
65
+
66
+ RestClient.send(*args,
67
+ {params: params}.merge(common_params(token, etag)),
68
+ &should_follow_redirect
69
+ )
70
+ rescue ::RestClient::Exception => exception
71
+ raise LastResponseError.new(exception.response)
72
+ end
73
+
74
+ def charging_path(path)
75
+ "#{Charging.configuration.url}#{path}"
76
+ end
77
+
78
+ def common_params(token, etag)
79
+ token = Charging.configuration.application_token if token === :use_application_token
80
+ request_headers = {
81
+ authorization: basic_credential_for('', token),
82
+ content_type: :json,
83
+ accept: :json,
84
+ user_agent: Charging.configuration.user_agent
85
+ }
86
+
87
+ request_headers['If-Match'] = etag if etag
88
+
89
+ request_headers
90
+ end
91
+
92
+ def encoded_body(body)
93
+ body.is_a?(Hash) ? MultiJson.encode(body) : body
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,191 @@
1
+ # encoding: utf-8
2
+
3
+ module Charging
4
+ class Invoice < Base
5
+ ATTRIBUTES = [
6
+ :kind, :amount, :document_number, :drawee, :due_date, :portfolio_code,
7
+ :charging_features, :supplier_name, :discount, :interest, :rebate,
8
+ :ticket, :protest_code, :protest_days, :instructions, :demonstrative,
9
+ :our_number
10
+ ]
11
+
12
+ READ_ONLY_ATTRIBUTES = [ :document_date, :paid ]
13
+
14
+ attr_accessor(*ATTRIBUTES)
15
+ attr_reader(*READ_ONLY_ATTRIBUTES, :domain, :charge_account)
16
+
17
+ def initialize(attributes, domain, charge_account, response = nil)
18
+ super(attributes, response)
19
+ @domain = domain
20
+ @charge_account = charge_account
21
+ end
22
+
23
+ # Creates current invoice at API.
24
+ #
25
+ # API method: <tt>POST /charge-accounts/:uuid/invoices/</tt>
26
+ #
27
+ # API documentation: https://charging.financeconnect.com.br/static/docs/charges.html#post-charge-accounts-uuid-invoices
28
+ def create!
29
+ super do
30
+ raise 'can not create without a domain' if invalid_domain?
31
+ raise 'can not create wihtout a charge account' if invalid_charge_account?
32
+
33
+ Invoice.post_charge_accounts_invoices(domain, charge_account, attributes)
34
+ end
35
+
36
+ reload_attributes!(Helpers.extract_uuid(last_response.headers[:location]) || uuid)
37
+ end
38
+
39
+ # Deletes the invoice at API
40
+ #
41
+ # API method: <tt>DELETE /invoices/:uuid/</tt>
42
+ #
43
+ # API documentation: https://charging.financeconnect.com.br/static/docs/charges.html#delete-invoices-uuid
44
+ def destroy!
45
+ super do
46
+ Http.delete("/invoices/#{uuid}/", domain.token, etag)
47
+ end
48
+ end
49
+
50
+ # Pays current invoice at API. You can pass <tt>paid_amount</tt>,
51
+ # <tt>payment_date</tt> and <tt>note</tt> about payment.
52
+ # Default values:
53
+ # - <tt>amount</tt>: amount
54
+ # - <tt>date</tt>: Time.now.strftime('%Y-%m-%d')
55
+ #
56
+ # API method: <tt>POST /invoices/:uuid/pay/</tt>
57
+ #
58
+ # API documentation: https://charging.financeconnect.com.br/static/docs/charges.html#post-invoices-uuid-pay
59
+ def pay!(payment_data = {})
60
+ reset_errors!
61
+
62
+ attributes = {
63
+ amount: self.amount,
64
+ date: Time.now.strftime('%Y-%m-%d')
65
+ }.merge(payment_data)
66
+
67
+ @last_response = Http.post("/invoices/#{uuid}/pay/", domain.token, MultiJson.encode(attributes), etag: self.etag)
68
+
69
+ raise_last_response_unless 201
70
+
71
+ reload_attributes!(uuid)
72
+ ensure
73
+ if $ERROR_INFO
74
+ @last_response = $ERROR_INFO.last_response if $ERROR_INFO.kind_of?(Http::LastResponseError)
75
+ @errors = [$ERROR_INFO.message]
76
+ end
77
+ end
78
+
79
+ # List all payments for an invoice
80
+ #
81
+ # API method: <tt>GET /invoices/:uuid/payments/</tt>
82
+ #
83
+ # API documentation: https://charging.financeconnect.com.br/static/docs/charges.html#get-invoices-uuid-payments
84
+ def payments
85
+ reset_errors!
86
+
87
+ response = Http.get("/invoices/#{uuid}/payments/", domain.token)
88
+
89
+ return [] if response.code != 200
90
+
91
+ MultiJson.decode(response.body)
92
+ end
93
+
94
+ # Returns a String with the temporary URL for print current invoice.
95
+ #
96
+ # API method: <tt>GET /invoices/:uuid/billet/</tt>
97
+ #
98
+ # API documentation: https://charging.financeconnect.com.br/static/docs/charges.html#get-invoices-uuid-billet
99
+ def billet_url
100
+ return if unpersisted?
101
+
102
+ response = Http.get("/invoices/#{uuid}/billet/", domain.token)
103
+
104
+ return if response.code != 200
105
+
106
+ MultiJson.decode(response.body)["billet"]
107
+ rescue
108
+ nil
109
+ end
110
+
111
+ # Finds an invoice by uuid. It requites an <tt>domain</tt> and a
112
+ # <tt>uuid</tt>.
113
+ #
114
+ # Returns an Invoice instance or raises a Http::LastResponseError if something
115
+ # went wrong, like unauthorized request, not found.
116
+ #
117
+ # API method: <tt>GET /invoices/:uuid/</tt>
118
+ #
119
+ # API documentation: https://charging.financeconnect.com.br/static/docs/charges.html#get-invoices-uuid
120
+ def self.find_by_uuid(domain, uuid)
121
+ Helpers.required_arguments!(domain: domain, uuid: uuid)
122
+
123
+ response = Invoice.get_invoice(domain, uuid)
124
+
125
+ raise_last_response_unless 200, response
126
+
127
+ load_persisted_invoice(MultiJson.decode(response.body), response, domain)
128
+ end
129
+
130
+ # Returns a list of kind of invoices available for current domain. You
131
+ # SHOULD pass a <tt>domain</tt> instance. You MAY pass a <tt>page</tt>
132
+ # and <tt>limit</tt> for pagination.
133
+ #
134
+ # API method: <tt>GET /invoices/kinds?page=:page&limit=:limit
135
+ #
136
+ # API documentation:
137
+ # https://charging.financeconnect.com.br/static/docs/charges.html#get-invoices-kinds-limit-limit-page-page
138
+ def self.kinds(domain, page = DEFAULT_PAGE, limit = DEFAULT_LIMIT)
139
+ Helpers.required_arguments!(domain: domain)
140
+
141
+ response = Http.get("/invoices/kinds/?page=#{Integer(page)}&limit=#{Integer(limit)}", domain.token)
142
+
143
+ raise_last_response_unless 200, response
144
+
145
+ MultiJson.decode(response)
146
+ end
147
+
148
+ def self.load_persisted_invoice(attributes, response, domain, charge_account = nil)
149
+ charge_account_uri = attributes.delete("charge_account").to_s
150
+
151
+ if charge_account.nil? && charge_account_uri.start_with?('http')
152
+ begin
153
+ charge_account = ChargeAccount.find_by_uri(domain, charge_account_uri)
154
+ rescue Http::LastResponseError
155
+ end
156
+ end
157
+
158
+ validate_attributes!(attributes)
159
+
160
+ Invoice.new(attributes, domain, charge_account, response)
161
+ end
162
+
163
+ private
164
+
165
+ def reload_attributes!(uuid)
166
+ new_invoice = self.class.find_by_uuid(domain, uuid)
167
+
168
+ (COMMON_ATTRIBUTES + READ_ONLY_ATTRIBUTES).each do |attribute|
169
+ instance_variable_set "@#{attribute}", new_invoice.send(attribute)
170
+ end
171
+
172
+ self
173
+ end
174
+
175
+ def self.get_invoice(domain, uuid)
176
+ Http.get("/invoices/#{uuid}/", domain.token)
177
+ end
178
+
179
+ def self.post_charge_accounts_invoices(domain, charge_account, attributes)
180
+ Http.post("/charge-accounts/#{charge_account.uuid}/invoices/", domain.token, MultiJson.encode(attributes))
181
+ end
182
+
183
+ def invalid_domain?
184
+ domain.nil?
185
+ end
186
+
187
+ def invalid_charge_account?
188
+ charge_account.nil?
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,50 @@
1
+ # encoding: utf-8
2
+
3
+ module Charging
4
+ # Represents a Charging service account.
5
+ class ServiceAccount
6
+ ATTRIBUTES = [:plan, :name, :uri, :uuid]
7
+
8
+ attr_accessor(*ATTRIBUTES)
9
+
10
+ # Responds the last http response from the API.
11
+ attr_reader :last_response
12
+
13
+ # Responds the current application token
14
+ attr_reader :application_token
15
+
16
+ def self.current
17
+ @current ||= find_by_token(Charging.configuration.application_token)
18
+ end
19
+
20
+ # Initializes a service account instance, to represent a charging account
21
+ def initialize(attributes, response, token) # :nodoc:
22
+ Helpers.load_variables(self, ATTRIBUTES, attributes)
23
+
24
+ @last_response = response
25
+ @application_token = token
26
+ end
27
+
28
+ # Finds a service account by it's access token. Returns the service account
29
+ # instance with all fields set if successful. If something went wrong, it
30
+ # raises Charging::Http::LastResponseError.
31
+ #
32
+ # API documentation: http://charging.financeconnect.com.br/static/docs/accounts_and_domains.html#get-account-entry-point
33
+ def self.find_by_token(token)
34
+ response = Http.get('/account/', token)
35
+
36
+ raise Http::LastResponseError.new(response) if response.code != 200
37
+
38
+ self.load_service_account_for response, token
39
+ rescue ::RestClient::Exception => exception
40
+ raise Http::LastResponseError.new(exception.response)
41
+ end
42
+
43
+ private
44
+
45
+ def self.load_service_account_for(response, token)
46
+ data = MultiJson.decode(response.body)
47
+ self.new(data, response, token)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+ module Charging
3
+ VERSION = "0.0.2"
4
+ end
@@ -0,0 +1,95 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Charging::ChargeAccount::Collection do
6
+ let(:domain_mock) { double(:domain, token: 'QNTGvpnYRVC4HbHibDBUIQ==') }
7
+
8
+ it 'should raise for invalid account' do
9
+ expected_error = [ArgumentError, 'domain required']
10
+
11
+ expect { described_class.new(nil, double(:response, code: 401)) }.to raise_error(*expected_error)
12
+ end
13
+
14
+ it 'should raise for invalid response' do
15
+ expected_error = [ArgumentError, 'response required']
16
+
17
+ expect { described_class.new(domain_mock, nil) }.to raise_error(*expected_error)
18
+ end
19
+
20
+ context 'with not success response' do
21
+ let(:response_not_found) { double(:response_not_found, code: 404) }
22
+
23
+ let!(:result) { described_class.new(domain_mock, response_not_found) }
24
+
25
+ it 'should have empty content' do
26
+ expect(result).to be_empty
27
+ end
28
+
29
+ it 'should have last response' do
30
+ expect(result.last_response).to eq response_not_found
31
+ end
32
+ end
33
+
34
+ context 'with success response without data' do
35
+ let(:response_success) do
36
+ double(code: 200, headers: {}, body: '[]')
37
+ end
38
+
39
+ let!(:result) { described_class.new(domain_mock, response_success) }
40
+
41
+ it 'should have empty content' do
42
+ expect(result).to be_empty
43
+ end
44
+
45
+ it 'should have last response' do
46
+ expect(result.last_response).to eq response_success
47
+ end
48
+ end
49
+
50
+ context 'with success response with data' do
51
+ let(:body) do
52
+ MultiJson.encode([{
53
+ account: { digit: "8", number: 1234 },
54
+ address: "new address",
55
+ advance_days: 10,
56
+ agency: { digit: "", number: 354 },
57
+ agreement_code: "1234",
58
+ bank: "237",
59
+ currency: 9,
60
+ etag: "9c8d4ad41a67770c79ace62b9515adf8b5b0a589",
61
+ name: "Conta de Cobrança no Bradesco",
62
+ national_identifier: "03.448.307/9170-25",
63
+ portfolio_code: "25",
64
+ sequence_numbers: [ 1, 9999999 ],
65
+ supplier_name: "Springfield Elemenary School",
66
+ uri: "http://sandbox.charging.financeconnect.com.br:8080/charge-accounts/29e77bc5-0e70-444c-a922-3149e78d905b/",
67
+ uuid: "29e77bc5-0e70-444c-a922-3149e78d905b"
68
+ }])
69
+ end
70
+
71
+ let(:response_success) do
72
+ double(:response, code: 200, headers: {}, body: body)
73
+ end
74
+
75
+ let(:result) { described_class.new(domain_mock, response_success) }
76
+
77
+ it 'should convert into an array of domain' do
78
+ expect(result.size).to eq 1
79
+ end
80
+
81
+ it 'should have last response' do
82
+ expect(result.last_response).to eq response_success
83
+ end
84
+
85
+ it 'should contain a domain' do
86
+ domain = result.first
87
+
88
+ expect(domain).to be_an_instance_of(Charging::ChargeAccount)
89
+ end
90
+
91
+ it 'should load current account for domain instance' do
92
+ expect(result.first.domain).to eq domain_mock
93
+ end
94
+ end
95
+ end