perata-sdk 0.2.0 → 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e636bdc22e1cf5cb23c0d1b1f93f3154362b6ada48dcc56e780847de36f578f7
4
- data.tar.gz: a29323a115321b71d10b6031b7170c6fda3e7d23e192ba1463e29afcc519d51f
3
+ metadata.gz: ffb21818636a4985aed2e37bed5c804482d4e33f36d59ac855170d95747dac5b
4
+ data.tar.gz: 2201ef7ba61a245ccf70aed0c7d48d11999f81fd8025a84acaf551ccec09441b
5
5
  SHA512:
6
- metadata.gz: 5b88a3f310e277f889147d95801565704b294120a2d6dc0f9809ced9c214753c8638b3b50e5deeb94fb2c785d811fe1759de836246b41e7652352c7aa4b92532
7
- data.tar.gz: b3f215424482e048dc8c24ef20262938d3f1808795573f91c9c4684529f3bd17b6992625d484106c5ebc40f92c258cdfe1a6319839e244926e7f10842300dd08
6
+ metadata.gz: e9d0be391bcc52e48eeee7fca3ac3ded3bd246aaf5aa595f7c568a231ed6195e7b70cb01bd12df25f2731d3e4ceece23f3c03cf7b9e7f233896f09ebb917e062
7
+ data.tar.gz: daee592161aed12eaabe847b9d8f43b4651ea18d8cf1ab98decf477343c1153fc356cc78d8443594c694c55cbab2c32b9a2226105b97bf5ab0982333db9749ee
data/README.md CHANGED
@@ -22,24 +22,52 @@ end
22
22
 
23
23
  | Serviço | Métodos |
24
24
  |---|---|
25
- | `perata.checkouts` | `create` |
25
+ | `perata.benefits` | `create`, `delete`, `get`, `list`, `list_iter`, `list_grants`, `list_grants_iter`, `update` |
26
+ | `perata.checkout_links` | `create`, `delete`, `get`, `list`, `list_iter`, `update` |
27
+ | `perata.checkouts` | `create`, `get`, `update` |
26
28
  | `perata.customer_sessions` | `create` |
27
- | `perata.customers` | `create`, `get`, `list`, `list_iter` |
28
- | `perata.orders` | `list`, `list_iter` |
29
+ | `perata.customers` | `create`, `delete`, `get`, `get_external`, `get_state`, `list`, `list_iter`, `update`, `update_external` |
30
+ | `perata.discounts` | `create`, `delete`, `get`, `list`, `list_iter`, `update` |
31
+ | `perata.disputes` | `get`, `list`, `list_iter` |
32
+ | `perata.events` | `list`, `list_iter` |
33
+ | `perata.meters` | `get`, `list`, `list_iter` |
34
+ | `perata.orders` | `get`, `get_invoice`, `get_receipt`, `list`, `list_iter` |
29
35
  | `perata.payment_methods` | `detach`, `list`, `list_iter` |
30
36
  | `perata.payments` | `get`, `list`, `list_iter` |
31
37
  | `perata.prices` | `create`, `list`, `list_iter` |
32
- | `perata.products` | `create`, `get`, `list`, `list_iter` |
33
- | `perata.refunds` | `create` |
34
- | `perata.subscriptions` | `cancel`, `get`, `list`, `list_iter` |
38
+ | `perata.products` | `archive`, `create`, `get`, `list`, `list_iter`, `update` |
39
+ | `perata.refunds` | `create`, `list`, `list_iter` |
40
+ | `perata.subscriptions` | `cancel`, `get`, `list`, `list_iter`, `update` |
41
+ | `perata.usage` | `ingest` |
42
+ | `perata.webhooks` | `create_endpoint`, `delete_endpoint`, `get_endpoint`, `list_deliveries`, `list_deliveries_iter`, `list_endpoints`, `list_endpoints_iter`, `update_endpoint` |
35
43
 
36
44
  Os modelos são objetos imutáveis com leitores em snake_case (`payment.amount_cents`). O corpo de uma escrita é a classe de parâmetros do método (`Perata::ProductsCreateParams.new(name: "Curso")`) ou um `Hash` com os mesmos nomes; um campo opcional não informado fica fora do JSON. Os métodos `_iter` devolvem um `Enumerator` que percorre todas as páginas a partir de `page:` (ou recebem um bloco).
37
45
 
38
- As escritas aceitam `idempotency_key:`, e todas aceitam `api_key:` para usar a chave de outra organização numa chamada. `Perata::Client.new` aceita `base_url:`, `timeout:` (30 segundos) e `transport:`.
46
+ As escritas aceitam `idempotency_key:`, e todas aceitam `api_key:` para usar a chave de outra organização numa chamada. `Perata::Client.new` aceita `base_url:`, `timeout:` (30 segundos) e `transport:`. Um POST sem chave de idempotência é enviado com uma `Idempotency-Key` própria (UUID v4 aleatório, uma por chamada); passe a sua para poder repetir a mesma operação com segurança.
47
+
48
+ ## Consumo (ingestão de eventos)
49
+
50
+ A ingestão de consumo usa outra credencial: a de uma fonte de ingestão (**Configurações › Fontes de ingestão**), `pus_test_…` em teste e `pus_live_…` em produção. Monte um cliente com ela; a chave de API da organização (`prk_…`) é recusada aqui com 401, e uma credencial de um ambiente nunca escreve no outro.
51
+
52
+ ```ruby
53
+ usage = Perata::Client.new(ENV.fetch("PERATA_USAGE_KEY")) # pus_test_… ou pus_live_…
54
+
55
+ receipt = usage.usage.ingest({ events: [{
56
+ customer_id: customer_id,
57
+ external_event_id: "requisicao-001", # a mesma fonte repetindo o id com o mesmo conteúdo é replay
58
+ input_tokens: "1200",
59
+ output_tokens: "300",
60
+ cached_tokens: "0",
61
+ occurred_at: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ") # segundos inteiros, até 90 dias atrás
62
+ }] })
63
+ # receipt.created, receipt.replayed
64
+ ```
65
+
66
+ De 1 a 100 eventos por chamada, até 64 KiB, todos gravados ou nenhum. Não há `Idempotency-Key` aqui: a identidade de um evento é o `external_event_id` da fonte, e repetir uma chamada é seguro (`replayed`); o mesmo id com outro conteúdo responde 409 (`conflict`), e um evento inválido, 422 (`invalid_request`) com `record` dizendo qual, a partir de 1.
39
67
 
40
68
  ## Erros
41
69
 
42
- Uma recusa da API levanta `Perata::Error`, com `status`, `code` (`invalid_request`, `not_found`…), `message`, `fields` (o problema de cada campo) e, num 429, `retry_after` em segundos.
70
+ Uma recusa da API levanta `Perata::Error`, com `status`, `code` (`invalid_request`, `not_found`…), `message`, `fields` (o problema de cada campo), `record` (na ingestão, o evento recusado) e, num 429, `retry_after` em segundos.
43
71
 
44
72
  ## Webhooks
45
73
 
@@ -56,4 +84,4 @@ A assinatura é a do Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webh
56
84
 
57
85
  O evento é tipado: um tipo para cada dado que o webhook carrega (`OrderWebhookEvent` traz um `Order`, `DisputeWebhookEvent` a disputa, `IdentifiersWebhookEvent` os identificadores…), e o `type` diz o que aconteceu; um tipo que esta versão do SDK ainda não conhece chega como `UnknownWebhookEvent`, com os dados como vieram. Durante a rotação do segredo o cabeçalho traz mais de uma assinatura, e basta uma conferir. `Perata.verify_webhook_signature` só confere a assinatura.
58
86
 
59
- Versão do SDK: 0.2.0 · contrato da API: 0.1.0
87
+ Versão do SDK: 0.4.0 · contrato da API: 0.1.0
data/lib/perata/client.rb CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  # Code generated by sdk/generator. DO NOT EDIT.
4
4
 
5
+ require "ipaddr"
5
6
  require "json"
6
7
  require "net/http"
8
+ require "securerandom"
7
9
  require "uri"
8
10
 
9
11
  module Perata
@@ -30,12 +32,24 @@ module Perata
30
32
  # product = perata.products.create({ name: "Curso" })
31
33
  # perata.products.list_iter { |product| puts product.name }
32
34
  class Client
35
+ # @return [BenefitsService]
36
+ attr_reader :benefits
37
+ # @return [CheckoutLinksService]
38
+ attr_reader :checkout_links
33
39
  # @return [CheckoutsService]
34
40
  attr_reader :checkouts
35
41
  # @return [CustomerSessionsService]
36
42
  attr_reader :customer_sessions
37
43
  # @return [CustomersService]
38
44
  attr_reader :customers
45
+ # @return [DiscountsService]
46
+ attr_reader :discounts
47
+ # @return [DisputesService]
48
+ attr_reader :disputes
49
+ # @return [EventsService]
50
+ attr_reader :events
51
+ # @return [MetersService]
52
+ attr_reader :meters
39
53
  # @return [OrdersService]
40
54
  attr_reader :orders
41
55
  # @return [PaymentMethodsService]
@@ -50,15 +64,28 @@ module Perata
50
64
  attr_reader :refunds
51
65
  # @return [SubscriptionsService]
52
66
  attr_reader :subscriptions
67
+ # @return [UsageService]
68
+ attr_reader :usage
69
+ # @return [WebhooksService]
70
+ attr_reader :webhooks
53
71
 
72
+ # The base URL must be https; http is accepted only for localhost,
73
+ # 127.0.0.0/8 and ::1, and any other raises ArgumentError.
54
74
  def initialize(api_key, base_url: DEFAULT_BASE_URL, timeout: 30, transport: NetHTTPTransport.new)
55
75
  @api_key = api_key
56
76
  @base_url = base_url.chomp("/")
77
+ check_base_url!(@base_url)
57
78
  @timeout = timeout
58
79
  @transport = transport
80
+ @benefits = BenefitsService.new(self)
81
+ @checkout_links = CheckoutLinksService.new(self)
59
82
  @checkouts = CheckoutsService.new(self)
60
83
  @customer_sessions = CustomerSessionsService.new(self)
61
84
  @customers = CustomersService.new(self)
85
+ @discounts = DiscountsService.new(self)
86
+ @disputes = DisputesService.new(self)
87
+ @events = EventsService.new(self)
88
+ @meters = MetersService.new(self)
62
89
  @orders = OrdersService.new(self)
63
90
  @payment_methods = PaymentMethodsService.new(self)
64
91
  @payments = PaymentsService.new(self)
@@ -66,6 +93,8 @@ module Perata
66
93
  @products = ProductsService.new(self)
67
94
  @refunds = RefundsService.new(self)
68
95
  @subscriptions = SubscriptionsService.new(self)
96
+ @usage = UsageService.new(self)
97
+ @webhooks = WebhooksService.new(self)
69
98
  end
70
99
 
71
100
  # Calls the API and answers what it wrapped in +data+; a refusal raises
@@ -84,11 +113,35 @@ module Perata
84
113
  data = JSON.generate(body)
85
114
  headers["Content-Type"] = "application/json"
86
115
  end
116
+ # A POST is sent with a key of its own, one a call, unless the caller gave one.
117
+ idempotency_key = SecureRandom.uuid if (idempotency_key.nil? || idempotency_key.empty?) && method == "POST"
87
118
  headers["Idempotency-Key"] = idempotency_key if idempotency_key && !idempotency_key.empty?
88
119
  status, response_headers, raw = @transport.call(method, url, headers, data, @timeout)
89
120
  raise Error.from_response(status, response_headers, raw) if status >= 300
90
121
 
122
+ return nil if status == 204
123
+
91
124
  JSON.parse(raw).fetch("data")
92
125
  end
126
+
127
+ private
128
+
129
+ def check_base_url!(base_url)
130
+ uri = begin
131
+ URI.parse(base_url)
132
+ rescue URI::InvalidURIError
133
+ nil
134
+ end
135
+ scheme = uri&.scheme.to_s.downcase
136
+ host = uri&.hostname.to_s.downcase
137
+ loopback = host == "localhost" || begin
138
+ IPAddr.new(host).loopback?
139
+ rescue IPAddr::Error
140
+ false
141
+ end
142
+ return if scheme == "https" || (scheme == "http" && loopback)
143
+
144
+ raise ArgumentError, "perata: the base URL must be https (http is accepted only for localhost, 127.0.0.0/8 and ::1), so the API key is never sent in clear text"
145
+ end
93
146
  end
94
147
  end
data/lib/perata/error.rb CHANGED
@@ -6,19 +6,22 @@ module Perata
6
6
  # A request the API refused.
7
7
  #
8
8
  # +code+ is the API's: invalid_credential, forbidden, not_found,
9
- # invalid_request, conflict, idempotency_conflict, body_too_large or
10
- # unavailable. +message+ is the API's explanation, +fields+ says, for
11
- # invalid_request, what is wrong with each field, and +retry_after+ is how
12
- # many seconds to wait on a 429.
9
+ # invalid_request, conflict, idempotency_conflict, unsupported_media_type, body_too_large, rate_limited or
10
+ # unavailable.
11
+ # +message+ is the API's explanation, +fields+ says, for invalid_request,
12
+ # what is wrong with each field, +record+ is, for the usage ingestion, the
13
+ # position (from 1) in the batch of the event the API refused (0 for any other
14
+ # error), and +retry_after+ is how many seconds to wait on a 429.
13
15
  class Error < StandardError
14
- attr_reader :status, :code, :message, :fields, :retry_after
16
+ attr_reader :status, :code, :message, :fields, :retry_after, :record
15
17
 
16
- def initialize(status, code, message = "", fields = {}, retry_after = nil)
18
+ def initialize(status, code, message = "", fields = {}, retry_after = nil, record = 0)
17
19
  @status = status
18
20
  @code = code
19
21
  @message = message
20
22
  @fields = fields
21
23
  @retry_after = retry_after
24
+ @record = record
22
25
  super(message.empty? ? "perata: #{status} #{code}" : "perata: #{status} #{code}: #{message}")
23
26
  end
24
27
 
@@ -35,9 +38,12 @@ module Perata
35
38
  rescue JSON::ParserError
36
39
  {} # A refusal without a JSON body is still a refusal.
37
40
  end
41
+ # Every error is the problem itself (error, message, fields). Servers from before
42
+ # that document wrapped the usage ingestion's errors in data: still read.
43
+ data = data["data"] if !data.key?("error") && data["data"].is_a?(Hash)
38
44
  retry_after = headers["retry-after"].to_s
39
45
  new(status, data["error"].to_s, data["message"].to_s, data["fields"] || {},
40
- retry_after.match?(/\A\d+\z/) ? retry_after.to_i : nil)
46
+ retry_after.match?(/\A\d+\z/) ? retry_after.to_i : nil, data["record"].is_a?(Integer) ? data["record"] : 0)
41
47
  end
42
48
  end
43
49
  end