charging-client 0.0.2

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.
@@ -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