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