c6_bank 0.2.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.
- checksums.yaml +7 -0
- data/.env.example +11 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +15 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +318 -0
- data/Rakefile +12 -0
- data/docs/NEXT_STEPS.md +17 -0
- data/docs/openapi/bolepix.yaml +850 -0
- data/docs/openapi/pix-api.yaml +4446 -0
- data/docs/rails_initializer_example.rb +17 -0
- data/docs/roteiro_mapping.md +31 -0
- data/examples/auth_probe.rb +14 -0
- data/examples/boleto_cases.rb +56 -0
- data/examples/env.rb +22 -0
- data/examples/live_sandbox.rb +90 -0
- data/examples/pix_cases.rb +42 -0
- data/exe/c6_bank +4 -0
- data/lib/c6_bank/boleto/request.rb +163 -0
- data/lib/c6_bank/client.rb +255 -0
- data/lib/c6_bank/configuration.rb +120 -0
- data/lib/c6_bank/errors.rb +33 -0
- data/lib/c6_bank/pix/payload.rb +132 -0
- data/lib/c6_bank/pix.rb +16 -0
- data/lib/c6_bank/resources/base.rb +34 -0
- data/lib/c6_bank/resources/boleto.rb +72 -0
- data/lib/c6_bank/resources/dda.rb +40 -0
- data/lib/c6_bank/resources/pix.rb +111 -0
- data/lib/c6_bank/resources.rb +11 -0
- data/lib/c6_bank/version.rb +5 -0
- data/lib/c6_bank.rb +68 -0
- data/sig/c6_bank.rbs +4 -0
- metadata +122 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# config/initializers/c6_bank.rb
|
|
2
|
+
#
|
|
3
|
+
# Store secrets in Rails credentials or environment variables.
|
|
4
|
+
# Never commit real certificates or client secrets.
|
|
5
|
+
|
|
6
|
+
C6Bank.configure do |c|
|
|
7
|
+
c.client_id = Rails.application.credentials.dig(:c6_bank, :client_id) || ENV.fetch("C6_CLIENT_ID")
|
|
8
|
+
c.client_secret = Rails.application.credentials.dig(:c6_bank, :client_secret) || ENV.fetch("C6_CLIENT_SECRET")
|
|
9
|
+
c.pix_key = Rails.application.credentials.dig(:c6_bank, :pix_key) || ENV["C6_PIX_KEY"]
|
|
10
|
+
|
|
11
|
+
c.certificate_path = Rails.application.credentials.dig(:c6_bank, :cert_path) || ENV.fetch("C6_CERT_PATH")
|
|
12
|
+
c.private_key_path = Rails.application.credentials.dig(:c6_bank, :key_path) || ENV.fetch("C6_KEY_PATH")
|
|
13
|
+
|
|
14
|
+
c.environment = Rails.env.production? ? :production : :sandbox
|
|
15
|
+
c.partner_software_name = "MyApp"
|
|
16
|
+
c.partner_software_version = "1.0.0"
|
|
17
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Roteiro de Conformidade → Gem
|
|
2
|
+
|
|
3
|
+
Paths below match the official C6 OpenAPI (`docs/openapi/bolepix.yaml`, `docs/openapi/pix-api.yaml`).
|
|
4
|
+
|
|
5
|
+
## Bolepix (`client.boletos`) — `/v2/bank_slips`
|
|
6
|
+
|
|
7
|
+
| Roteiro | Method |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 1–3 Emitir (simples, juros/multa, desconto) | `create` / `C6Bank::Boleto::Request` (`with_fees`, `with_juros`, `with_multa`, `add_desconto`) |
|
|
10
|
+
| 4 Baixa | `cancel` / `baixa` → `PUT /v2/bank_slips/{id}/cancel` |
|
|
11
|
+
| 5 Alterar | `update` → `PATCH /v2/bank_slips/{id}` |
|
|
12
|
+
| 6 Consultar | `find` → `GET /v2/bank_slips/{id}` |
|
|
13
|
+
| Listar | `list` → `GET /v2/bank_slips/list` |
|
|
14
|
+
| PDF | `pdf` → `GET /v2/bank_slips/{id}/pdf` |
|
|
15
|
+
|
|
16
|
+
Pix QR on a boleto: set `C6_PIX_KEY` (EVP). Opt out with `req.without_pix`.
|
|
17
|
+
|
|
18
|
+
## Pix (`client.pix`) — `/v2/pix`
|
|
19
|
+
|
|
20
|
+
| Roteiro | Method | HTTP |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| 7.1 / 7.2 Cobrança imediata | `create_immediate_charge` | `POST /cob` or `PUT /cob/{txid}` |
|
|
23
|
+
| 7.3 Consultar cob | `find_immediate` | `GET /cob/{txid}` |
|
|
24
|
+
| 7.4 Listar cob | `list_immediate` | `GET /cob` |
|
|
25
|
+
| 7.5 Cobrança com vencimento | `create_due_charge` | `PUT /cobv/{txid}` |
|
|
26
|
+
| 7.6 / 7.7 Consultar cobv | `find_due` / `list_due` | `GET /cobv/{txid}`, `GET /cobv` |
|
|
27
|
+
| 7.8 Webhook | `configure_webhook` | `PUT /webhook/{chave}` |
|
|
28
|
+
|
|
29
|
+
## Payments / DDA (`client.dda`)
|
|
30
|
+
|
|
31
|
+
Still a thin wrapper. Confirm paths in the Agendamento de Pagamentos spec before homologating section 8.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require_relative "env"
|
|
6
|
+
require "c6_bank"
|
|
7
|
+
|
|
8
|
+
C6BankExamples.load_env!
|
|
9
|
+
|
|
10
|
+
client = C6Bank::Client.from_env
|
|
11
|
+
client.authenticate!
|
|
12
|
+
puts "Authenticated. scope=#{client.scope.inspect}"
|
|
13
|
+
rescue C6Bank::AuthenticationError => e
|
|
14
|
+
abort e.message
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "date"
|
|
6
|
+
require "json"
|
|
7
|
+
require_relative "env"
|
|
8
|
+
require "c6_bank"
|
|
9
|
+
|
|
10
|
+
C6BankExamples.load_env!
|
|
11
|
+
|
|
12
|
+
client = C6Bank::Client.from_env
|
|
13
|
+
client.authenticate!
|
|
14
|
+
|
|
15
|
+
due_date = (Date.today + 10).iso8601
|
|
16
|
+
|
|
17
|
+
puts "=== Bolepix with Pix (default when C6_PIX_KEY is set) ==="
|
|
18
|
+
req = C6Bank.boleto_request(
|
|
19
|
+
amount: 150.75,
|
|
20
|
+
due_date: due_date,
|
|
21
|
+
description: "Pedido 42"
|
|
22
|
+
)
|
|
23
|
+
req.payer(
|
|
24
|
+
name: "Fulano de Tal",
|
|
25
|
+
tax_id: "12345678909",
|
|
26
|
+
address: {
|
|
27
|
+
address: "Av. Nove de Julho, 3186",
|
|
28
|
+
neighborhood: "Jardins",
|
|
29
|
+
city: "Sao Paulo",
|
|
30
|
+
state: "SP",
|
|
31
|
+
zip_code: "01406000"
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
req.with_fees(
|
|
35
|
+
fine_value: 10,
|
|
36
|
+
fine_deadline: 5,
|
|
37
|
+
fine_type: "FIXED_VALUE",
|
|
38
|
+
interest_value: 0.5,
|
|
39
|
+
interest_deadline: 1,
|
|
40
|
+
interest_type: "VALUE_PER_DAY"
|
|
41
|
+
)
|
|
42
|
+
req.bank_slip(instructions: ["Nao receber apos o vencimento"])
|
|
43
|
+
|
|
44
|
+
puts JSON.pretty_generate(req.to_h.merge("payment_method" => req.to_h[:payment_method]))
|
|
45
|
+
puts
|
|
46
|
+
puts client.boletos.create(req)
|
|
47
|
+
|
|
48
|
+
puts "\n=== Boleto only ==="
|
|
49
|
+
req2 = C6Bank.boleto_request(amount: 99.90, due_date: due_date, description: "Somente boleto")
|
|
50
|
+
req2.payer(
|
|
51
|
+
name: "Empresa Exemplo Ltda",
|
|
52
|
+
tax_id: "12345678000195",
|
|
53
|
+
address: { address: "Rua Teste 100", city: "Curitiba", state: "PR", zip_code: "80010000" }
|
|
54
|
+
)
|
|
55
|
+
req2.without_pix
|
|
56
|
+
puts client.boletos.create(req2)
|
data/examples/env.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module C6BankExamples
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def load_env!(path = File.expand_path("../.env", __dir__))
|
|
7
|
+
return unless File.file?(path)
|
|
8
|
+
|
|
9
|
+
File.readlines(path, chomp: true).each do |line|
|
|
10
|
+
next if line.strip.empty? || line.lstrip.start_with?("#")
|
|
11
|
+
|
|
12
|
+
key, value = line.split("=", 2)
|
|
13
|
+
next unless key && value
|
|
14
|
+
|
|
15
|
+
ENV[key] = unquote(value) if ENV[key].nil? || ENV[key].empty?
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def unquote(value)
|
|
20
|
+
value.strip.sub(/\A(['"])(.*)\1\z/, '\2')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Hits the C6 sandbox with the credentials in `.env`.
|
|
5
|
+
#
|
|
6
|
+
# bundle exec ruby examples/live_sandbox.rb
|
|
7
|
+
#
|
|
8
|
+
# Sandbox hours: Monday–Friday 07:00–22:00 BRT.
|
|
9
|
+
|
|
10
|
+
require "bundler/setup"
|
|
11
|
+
require "date"
|
|
12
|
+
require "json"
|
|
13
|
+
require_relative "env"
|
|
14
|
+
require "c6_bank"
|
|
15
|
+
|
|
16
|
+
$stdout.sync = true
|
|
17
|
+
|
|
18
|
+
C6BankExamples.load_env!
|
|
19
|
+
|
|
20
|
+
client = C6Bank::Client.from_env
|
|
21
|
+
|
|
22
|
+
puts "Environment : #{client.config.environment}"
|
|
23
|
+
puts "Base URL : #{client.config.effective_base_url}"
|
|
24
|
+
puts "Client ID : #{client.config.client_id[0, 8]}…"
|
|
25
|
+
puts "Pix key : #{client.config.pix_key}"
|
|
26
|
+
puts "Wallet : #{client.config.effective_billing_scheme}"
|
|
27
|
+
puts
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
client.authenticate!
|
|
31
|
+
puts "Auth OK"
|
|
32
|
+
puts "Scope : #{client.scope.inspect}"
|
|
33
|
+
rescue C6Bank::AuthenticationError => e
|
|
34
|
+
abort <<~MSG
|
|
35
|
+
Auth failed: #{e.message}
|
|
36
|
+
|
|
37
|
+
Typical causes:
|
|
38
|
+
- mTLS certificate expired or not issued for this client_id
|
|
39
|
+
- sandbox closed (Mon-Fri 07:00-22:00 BRT)
|
|
40
|
+
- client_id / client_secret mismatch
|
|
41
|
+
MSG
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
due_date = (Date.today + 10).iso8601
|
|
45
|
+
|
|
46
|
+
puts "\n=== Bolepix (boleto + Pix QR) ==="
|
|
47
|
+
req = C6Bank.boleto_request(
|
|
48
|
+
amount: 12.34,
|
|
49
|
+
due_date: due_date,
|
|
50
|
+
description: "c6_bank gem live sandbox"
|
|
51
|
+
)
|
|
52
|
+
req.payer(
|
|
53
|
+
name: "Fulano de Tal",
|
|
54
|
+
tax_id: "12345678909",
|
|
55
|
+
address: {
|
|
56
|
+
address: "Av. Nove de Julho, 3186",
|
|
57
|
+
neighborhood: "Jardins",
|
|
58
|
+
city: "Sao Paulo",
|
|
59
|
+
state: "SP",
|
|
60
|
+
zip_code: "01406000"
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
req.bank_slip(instructions: ["Pagavel via boleto ou Pix"])
|
|
64
|
+
|
|
65
|
+
begin
|
|
66
|
+
bolepix = client.boletos.create(req)
|
|
67
|
+
puts JSON.pretty_generate(bolepix)
|
|
68
|
+
pix = bolepix.dig("payment_method", "pix") || bolepix.dig(:payment_method, :pix)
|
|
69
|
+
if pix
|
|
70
|
+
puts "Pix QR present: #{pix['qr_code'] || pix[:qr_code] ? 'yes' : 'no'}"
|
|
71
|
+
puts "Pix reference : #{pix['reference'] || pix[:reference]}"
|
|
72
|
+
else
|
|
73
|
+
puts "WARN: charge created without Pix. Check that C6_PIX_KEY is a registered EVP key."
|
|
74
|
+
end
|
|
75
|
+
rescue C6Bank::ApiError => e
|
|
76
|
+
puts "Bolepix failed (#{e.status}): #{e.body}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
puts "\n=== Pix cob (immediate charge) ==="
|
|
80
|
+
begin
|
|
81
|
+
cob = client.pix.create_immediate_charge(
|
|
82
|
+
amount: "5.00",
|
|
83
|
+
expiration: 3600,
|
|
84
|
+
devedor: { cpf: "12345678909", nome: "Fulano de Tal" },
|
|
85
|
+
solicitacaoPagador: "c6_bank gem live sandbox"
|
|
86
|
+
)
|
|
87
|
+
puts JSON.pretty_generate(cob)
|
|
88
|
+
rescue C6Bank::ApiError => e
|
|
89
|
+
puts "Pix cob failed (#{e.status}): #{e.body}"
|
|
90
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "date"
|
|
6
|
+
require "json"
|
|
7
|
+
require_relative "env"
|
|
8
|
+
require "c6_bank"
|
|
9
|
+
|
|
10
|
+
C6BankExamples.load_env!
|
|
11
|
+
|
|
12
|
+
client = C6Bank::Client.from_env
|
|
13
|
+
client.authenticate!
|
|
14
|
+
|
|
15
|
+
puts "=== Immediate Pix charge (cob) ==="
|
|
16
|
+
cob = client.pix.create_immediate_charge(
|
|
17
|
+
amount: "89.90",
|
|
18
|
+
expiration: 3600,
|
|
19
|
+
devedor: { cpf: "12345678909", nome: "Fulano" },
|
|
20
|
+
solicitacaoPagador: "Servico prestado"
|
|
21
|
+
)
|
|
22
|
+
puts JSON.pretty_generate(cob)
|
|
23
|
+
|
|
24
|
+
puts "\n=== Due Pix charge (cobv) ==="
|
|
25
|
+
cobv = client.pix.create_due_charge(
|
|
26
|
+
amount: "250.00",
|
|
27
|
+
due_date: (Date.today + 15).iso8601,
|
|
28
|
+
days_after_due: 30,
|
|
29
|
+
devedor: {
|
|
30
|
+
cpf: "12345678909",
|
|
31
|
+
nome: "Fulano de Tal",
|
|
32
|
+
logradouro: "Alameda Souza, 80",
|
|
33
|
+
cidade: "Recife",
|
|
34
|
+
uf: "PE",
|
|
35
|
+
cep: "70011750"
|
|
36
|
+
},
|
|
37
|
+
solicitacaoPagador: "Cobranca com vencimento"
|
|
38
|
+
)
|
|
39
|
+
puts JSON.pretty_generate(cobv)
|
|
40
|
+
|
|
41
|
+
puts "\nConsult by txid:"
|
|
42
|
+
puts JSON.pretty_generate(client.pix.find_immediate(cob.fetch("txid")))
|
data/exe/c6_bank
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module C6Bank
|
|
4
|
+
module Boleto
|
|
5
|
+
# Builder for C6 Bank Bolepix (bank slip + optional Pix QR) requests.
|
|
6
|
+
#
|
|
7
|
+
# Matches the official OpenAPI spec (docs/openapi/bolepix.yaml).
|
|
8
|
+
class Request
|
|
9
|
+
attr_reader :data
|
|
10
|
+
|
|
11
|
+
def self.generate_external_reference_id
|
|
12
|
+
require "securerandom"
|
|
13
|
+
SecureRandom.alphanumeric(26).upcase
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.apply_defaults(body, config, omit_pix: false)
|
|
17
|
+
payload = C6Bank::Pix::Payload.stringify(body)
|
|
18
|
+
payment_method = payload["payment_method"] ||= {}
|
|
19
|
+
bank_slip = payment_method["bank_slip"] ||= {}
|
|
20
|
+
bank_slip["billing_scheme"] ||= config.effective_billing_scheme
|
|
21
|
+
|
|
22
|
+
if omit_pix
|
|
23
|
+
payment_method.delete("pix")
|
|
24
|
+
elsif payment_method["pix"].nil? && config.pix_key
|
|
25
|
+
payment_method["pix"] = { "key" => config.pix_key, "type" => "EVP" }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
payload["external_reference_id"] ||= generate_external_reference_id
|
|
29
|
+
payload
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(params = {})
|
|
33
|
+
@omit_pix = false
|
|
34
|
+
@data = {
|
|
35
|
+
external_reference_id: params[:external_reference_id] || params["external_reference_id"],
|
|
36
|
+
amount: params[:amount] || params[:valor] || params["amount"] || params["valor"],
|
|
37
|
+
due_date: params[:due_date] || params[:data_vencimento] || params["due_date"] || params["data_vencimento"],
|
|
38
|
+
description: params[:description] || params["description"],
|
|
39
|
+
days_after_due_date: params[:days_after_due_date] || params["days_after_due_date"],
|
|
40
|
+
origin: params[:origin] || params["origin"],
|
|
41
|
+
payer: normalize_payer(params[:payer] || params["payer"]),
|
|
42
|
+
fees: normalize_fees(params[:fees] || params["fees"]),
|
|
43
|
+
payment_method: {
|
|
44
|
+
bank_slip: {}
|
|
45
|
+
}
|
|
46
|
+
}.compact
|
|
47
|
+
|
|
48
|
+
return unless params[:payment_method] || params["payment_method"]
|
|
49
|
+
|
|
50
|
+
@data[:payment_method] = (params[:payment_method] || params["payment_method"]).dup
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def omit_pix?
|
|
54
|
+
@omit_pix
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def payer(name: nil, tax_id: nil, email: nil, address: nil, **rest)
|
|
58
|
+
payer_data = {
|
|
59
|
+
name: name,
|
|
60
|
+
tax_id: tax_id,
|
|
61
|
+
email: email,
|
|
62
|
+
address: address
|
|
63
|
+
}.merge(rest).compact
|
|
64
|
+
@data[:payer] = normalize_payer(payer_data)
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def with_fees(**fee_params)
|
|
69
|
+
@data[:fees] = normalize_fees(fee_params)
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def bank_slip(billing_scheme: nil, our_number: nil, your_number: nil, instructions: nil, **rest)
|
|
74
|
+
@data[:payment_method] ||= {}
|
|
75
|
+
bs = @data[:payment_method][:bank_slip] ||= {}
|
|
76
|
+
bs[:billing_scheme] = billing_scheme.to_s if billing_scheme
|
|
77
|
+
bs[:our_number] = our_number.to_s if our_number
|
|
78
|
+
bs[:your_number] = your_number.to_s if your_number
|
|
79
|
+
bs[:instructions] = Array(instructions) if instructions
|
|
80
|
+
bs.merge!(rest.transform_keys(&:to_sym))
|
|
81
|
+
self
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def pix(key:, type: "EVP")
|
|
85
|
+
@omit_pix = false
|
|
86
|
+
@data[:payment_method] ||= {}
|
|
87
|
+
@data[:payment_method][:pix] = { key: key, type: type }
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def without_pix
|
|
92
|
+
@omit_pix = true
|
|
93
|
+
@data[:payment_method]&.delete(:pix)
|
|
94
|
+
self
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def with_juros(valor:, tipo: :diario)
|
|
98
|
+
tipo_str = case tipo.to_s.downcase
|
|
99
|
+
when "diario", "value_per_day" then "VALUE_PER_DAY"
|
|
100
|
+
when "monthly_percentage", "mensal" then "MONTHLY_PERCENTAGE"
|
|
101
|
+
else tipo.to_s.upcase
|
|
102
|
+
end
|
|
103
|
+
fees = @data[:fees] || {}
|
|
104
|
+
fees[:interest_value] = valor
|
|
105
|
+
fees[:interest_type] = tipo_str
|
|
106
|
+
@data[:fees] = fees
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def with_multa(tipo:, valor:)
|
|
111
|
+
tipo_str = case tipo.to_s.downcase
|
|
112
|
+
when "fixo", "fixed" then "FIXED_VALUE"
|
|
113
|
+
when "percentual", "percentage", "%" then "PERCENTAGE"
|
|
114
|
+
else tipo.to_s.upcase
|
|
115
|
+
end
|
|
116
|
+
fees = @data[:fees] || {}
|
|
117
|
+
fees[:fine_value] = valor
|
|
118
|
+
fees[:fine_type] = tipo_str
|
|
119
|
+
@data[:fees] = fees
|
|
120
|
+
self
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def add_desconto(**opts)
|
|
124
|
+
fees = @data[:fees] ||= {}
|
|
125
|
+
fees[:first_discount_value] = opts[:valor] || opts[:value] if opts[:valor] || opts[:value]
|
|
126
|
+
fees[:first_discount_deadline] = opts[:dias_antes] || opts[:deadline] if opts[:dias_antes] || opts[:deadline]
|
|
127
|
+
if opts[:tipo] || opts[:type]
|
|
128
|
+
t = (opts[:tipo] || opts[:type]).to_s.upcase
|
|
129
|
+
fees[:discount_type] = t
|
|
130
|
+
end
|
|
131
|
+
self
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def to_h
|
|
135
|
+
result = @data.dup
|
|
136
|
+
if result[:payment_method].is_a?(Hash)
|
|
137
|
+
result[:payment_method] = result[:payment_method].dup
|
|
138
|
+
result[:payment_method].delete(:pix) if result[:payment_method][:pix].nil? || omit_pix?
|
|
139
|
+
end
|
|
140
|
+
result.compact
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def normalize_payer(payer)
|
|
146
|
+
return nil unless payer
|
|
147
|
+
|
|
148
|
+
p = payer.is_a?(Hash) ? payer.dup : {}
|
|
149
|
+
p[:tax_id] ||= p.delete(:cpf_cnpj) || p.delete("cpf_cnpj")
|
|
150
|
+
addr = p[:address] || p["address"]
|
|
151
|
+
p[:address] = addr.transform_keys(&:to_sym) if addr.is_a?(Hash)
|
|
152
|
+
p.transform_keys(&:to_sym).compact
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def normalize_fees(fees)
|
|
156
|
+
return nil unless fees
|
|
157
|
+
|
|
158
|
+
f = fees.is_a?(Hash) ? fees.dup : {}
|
|
159
|
+
f.transform_keys(&:to_sym).compact
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "faraday/retry"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "json"
|
|
7
|
+
require "time"
|
|
8
|
+
require "uri"
|
|
9
|
+
|
|
10
|
+
module C6Bank
|
|
11
|
+
class Client
|
|
12
|
+
TOKEN_PATHS = ["/v1/auth", "/v1/auth/"].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :config, :scope
|
|
15
|
+
|
|
16
|
+
def self.from_env(**overrides)
|
|
17
|
+
new(
|
|
18
|
+
{
|
|
19
|
+
client_id: ENV.fetch("C6_CLIENT_ID"),
|
|
20
|
+
client_secret: ENV.fetch("C6_CLIENT_SECRET"),
|
|
21
|
+
certificate_path: ENV.fetch("C6_CERT_PATH"),
|
|
22
|
+
private_key_path: ENV.fetch("C6_KEY_PATH"),
|
|
23
|
+
pix_key: ENV["C6_PIX_KEY"],
|
|
24
|
+
environment: (ENV["C6_ENV"] || "sandbox").to_sym,
|
|
25
|
+
partner_software_name: ENV["C6_PARTNER_SOFTWARE_NAME"],
|
|
26
|
+
partner_software_version: ENV["C6_PARTNER_SOFTWARE_VERSION"],
|
|
27
|
+
base_url: ENV["C6_BASE_URL"],
|
|
28
|
+
auth_base_url: ENV["C6_AUTH_BASE_URL"],
|
|
29
|
+
token_path: ENV["C6_TOKEN_PATH"],
|
|
30
|
+
boleto_path: ENV["C6_BOLETO_PATH"],
|
|
31
|
+
pix_path: ENV["C6_PIX_PATH"]
|
|
32
|
+
}.compact.merge(overrides)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(options = {})
|
|
37
|
+
@config = Configuration.new
|
|
38
|
+
|
|
39
|
+
options.each do |key, value|
|
|
40
|
+
setter = "#{key}="
|
|
41
|
+
@config.public_send(setter, value) if @config.respond_to?(setter)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
@config.validate!
|
|
45
|
+
|
|
46
|
+
@token = nil
|
|
47
|
+
@token_expires_at = nil
|
|
48
|
+
@scope = nil
|
|
49
|
+
@preferred_token_path = options[:token_path] || @config.token_path
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def connection
|
|
53
|
+
@connection ||= build_connection
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def boletos
|
|
57
|
+
Resources::Boleto.new(self)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def pix
|
|
61
|
+
Resources::Pix.new(self)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dda
|
|
65
|
+
Resources::Dda.new(self)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def request(method, path, body: nil, params: nil, headers: {}, raw: false)
|
|
69
|
+
ensure_authenticated!
|
|
70
|
+
|
|
71
|
+
response = perform_request(method, path, body: body, params: params, headers: headers)
|
|
72
|
+
begin
|
|
73
|
+
raw ? raw_or_error(response) : handle_response(response)
|
|
74
|
+
rescue AuthenticationError
|
|
75
|
+
raise unless response.status == 401
|
|
76
|
+
|
|
77
|
+
@token = nil
|
|
78
|
+
@token_expires_at = nil
|
|
79
|
+
ensure_authenticated!
|
|
80
|
+
response = perform_request(method, path, body: body, params: params, headers: headers)
|
|
81
|
+
raw ? raw_or_error(response) : handle_response(response)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def authenticate!(token_path: nil)
|
|
86
|
+
@config.validate!
|
|
87
|
+
|
|
88
|
+
auth_base = @config.effective_auth_base_url
|
|
89
|
+
token_conn = Faraday.new(url: auth_base) do |f|
|
|
90
|
+
f.ssl.merge!(ssl_options)
|
|
91
|
+
f.request :retry, max: @config.max_retries, interval: 0.5, interval_randomness: 0.5, backoff_factor: 2,
|
|
92
|
+
methods: %i[post], retry_statuses: [429, 500, 502, 503, 504]
|
|
93
|
+
f.options.open_timeout = @config.open_timeout
|
|
94
|
+
f.options.timeout = @config.read_timeout
|
|
95
|
+
f.adapter Faraday.default_adapter
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
paths = if token_path
|
|
99
|
+
[token_path]
|
|
100
|
+
else
|
|
101
|
+
[@preferred_token_path, *TOKEN_PATHS].compact.uniq
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
last_error = nil
|
|
105
|
+
paths.each do |path|
|
|
106
|
+
resp = token_conn.post(path) do |req|
|
|
107
|
+
req.headers.merge!(
|
|
108
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
|
109
|
+
"Accept" => "application/json"
|
|
110
|
+
)
|
|
111
|
+
req.body = URI.encode_www_form(
|
|
112
|
+
grant_type: "client_credentials",
|
|
113
|
+
client_id: @config.client_id,
|
|
114
|
+
client_secret: @config.client_secret
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if resp.status.between?(200, 299)
|
|
119
|
+
store_token(resp)
|
|
120
|
+
@preferred_token_path = path
|
|
121
|
+
return true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
last_error = AuthenticationError.new("Could not obtain token (#{resp.status}): #{utf8(resp.body)}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
raise last_error if last_error
|
|
128
|
+
|
|
129
|
+
raise AuthenticationError, "Could not obtain token"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def authenticated?
|
|
133
|
+
@token && @token_expires_at && Time.now < @token_expires_at
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def set_token(access_token, expires_in: 3600)
|
|
137
|
+
@token = access_token
|
|
138
|
+
@token_expires_at = Time.now + expires_in.to_i - 30
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def auth_headers(extra = {})
|
|
142
|
+
h = {}
|
|
143
|
+
h["Authorization"] = "Bearer #{@token}" if @token
|
|
144
|
+
h["partner-software-name"] = @config.partner_software_name if @config.partner_software_name
|
|
145
|
+
h["partner-software-version"] = @config.partner_software_version if @config.partner_software_version
|
|
146
|
+
h.merge(extra)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def ensure_authenticated!
|
|
152
|
+
authenticate! unless authenticated?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def store_token(resp)
|
|
156
|
+
parsed = begin
|
|
157
|
+
JSON.parse(resp.body.to_s)
|
|
158
|
+
rescue JSON::ParserError
|
|
159
|
+
{}
|
|
160
|
+
end
|
|
161
|
+
token = parsed["access_token"] || parsed["token"] || parsed["accessToken"]
|
|
162
|
+
|
|
163
|
+
raise AuthenticationError, "Response did not include access_token. Body: #{resp.body}" unless token
|
|
164
|
+
|
|
165
|
+
@token = token
|
|
166
|
+
@scope = parsed["scope"]
|
|
167
|
+
expires = parsed["expires_in"] || parsed["expiresIn"] || 3600
|
|
168
|
+
@token_expires_at = Time.now + expires.to_i - 30
|
|
169
|
+
true
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def build_connection
|
|
173
|
+
Faraday.new(url: @config.effective_base_url) do |f|
|
|
174
|
+
f.ssl.merge!(ssl_options)
|
|
175
|
+
f.request :retry, max: @config.max_retries,
|
|
176
|
+
interval: 0.5,
|
|
177
|
+
interval_randomness: 0.5,
|
|
178
|
+
backoff_factor: 2,
|
|
179
|
+
methods: %i[get head options],
|
|
180
|
+
retry_statuses: [429, 500, 502, 503, 504]
|
|
181
|
+
f.options.open_timeout = @config.open_timeout
|
|
182
|
+
f.options.timeout = @config.read_timeout
|
|
183
|
+
f.adapter Faraday.default_adapter
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def ssl_options
|
|
188
|
+
{
|
|
189
|
+
client_cert: @config.ssl_certificate,
|
|
190
|
+
client_key: @config.ssl_private_key,
|
|
191
|
+
verify: true
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def perform_request(method, path, body:, params:, headers:)
|
|
196
|
+
request_headers = {
|
|
197
|
+
"Accept" => "application/json"
|
|
198
|
+
}.merge(headers)
|
|
199
|
+
|
|
200
|
+
request_headers["Content-Type"] ||= "application/json" if body
|
|
201
|
+
request_headers["Authorization"] = "Bearer #{@token}" if @token
|
|
202
|
+
request_headers["partner-software-name"] = @config.partner_software_name if @config.partner_software_name
|
|
203
|
+
request_headers["partner-software-version"] = @config.partner_software_version if @config.partner_software_version
|
|
204
|
+
|
|
205
|
+
connection.public_send(method, path) do |req|
|
|
206
|
+
req.params.update(params) if params
|
|
207
|
+
req.body = json_body(body) unless body.nil?
|
|
208
|
+
req.headers.merge!(request_headers)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def json_body(body)
|
|
213
|
+
return body if body.is_a?(String)
|
|
214
|
+
|
|
215
|
+
JSON.generate(body)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def raw_or_error(response)
|
|
219
|
+
return response if response.status.between?(200, 299)
|
|
220
|
+
|
|
221
|
+
handle_response(response)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def utf8(value)
|
|
225
|
+
str = value.to_s.dup
|
|
226
|
+
str.force_encoding("UTF-8")
|
|
227
|
+
str.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def handle_response(response)
|
|
231
|
+
case response.status
|
|
232
|
+
when 200..299
|
|
233
|
+
return nil if response.body.to_s.strip.empty?
|
|
234
|
+
|
|
235
|
+
begin
|
|
236
|
+
JSON.parse(response.body)
|
|
237
|
+
rescue JSON::ParserError
|
|
238
|
+
utf8(response.body)
|
|
239
|
+
end
|
|
240
|
+
when 401
|
|
241
|
+
@token = nil
|
|
242
|
+
@token_expires_at = nil
|
|
243
|
+
raise AuthenticationError, "Unauthorized (401): #{utf8(response.body)}"
|
|
244
|
+
when 404
|
|
245
|
+
raise NotFoundError.new("Not found", status: 404, body: utf8(response.body), response: response)
|
|
246
|
+
when 429
|
|
247
|
+
raise RateLimitError.new("Rate limited", status: 429, body: utf8(response.body), response: response)
|
|
248
|
+
when 500..599
|
|
249
|
+
raise ServerError.new("Server error", status: response.status, body: utf8(response.body), response: response)
|
|
250
|
+
else
|
|
251
|
+
raise ApiError.new("API error", status: response.status, body: utf8(response.body), response: response)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|