charging-client 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +1 -0
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +81 -0
- data/LICENSE +191 -0
- data/README.rdoc +228 -0
- data/Rakefile +16 -0
- data/charging-client.gemspec +40 -0
- data/lib/charging.rb +36 -0
- data/lib/charging/base.rb +109 -0
- data/lib/charging/charge_account.rb +169 -0
- data/lib/charging/collection.rb +29 -0
- data/lib/charging/configuration.rb +28 -0
- data/lib/charging/domain.rb +164 -0
- data/lib/charging/helpers.rb +37 -0
- data/lib/charging/http.rb +96 -0
- data/lib/charging/invoice.rb +191 -0
- data/lib/charging/service_account.rb +50 -0
- data/lib/charging/version.rb +4 -0
- data/spec/charging/charge_account_collection_spec.rb +95 -0
- data/spec/charging/charge_account_spec.rb +289 -0
- data/spec/charging/configuration_spec.rb +61 -0
- data/spec/charging/domain_collection_spec.rb +101 -0
- data/spec/charging/domain_spec.rb +386 -0
- data/spec/charging/helpers_spec.rb +59 -0
- data/spec/charging/http_last_response_error_spec.rb +18 -0
- data/spec/charging/http_spec.rb +184 -0
- data/spec/charging/invoice_spec.rb +442 -0
- data/spec/charging/service_account_spec.rb +71 -0
- data/spec/charging_spec.rb +42 -0
- data/spec/fixtures/new_user.json +8 -0
- data/spec/fixtures/recreate_user.json +9 -0
- data/spec/spec_helper.rb +69 -0
- data/spec/support/factory.rb +32 -0
- data/spec/support/faker.rb +56 -0
- metadata +283 -0
@@ -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,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
|