perata-sdk 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64384c4af37e5af51b1adea727b0475da68c843b5c25aedb6392bd3b3cd4bc8a
4
- data.tar.gz: b87c39face8ac30151097de8cc07b5be89787420501ea236afc5f4d875f77348
3
+ metadata.gz: e636bdc22e1cf5cb23c0d1b1f93f3154362b6ada48dcc56e780847de36f578f7
4
+ data.tar.gz: a29323a115321b71d10b6031b7170c6fda3e7d23e192ba1463e29afcc519d51f
5
5
  SHA512:
6
- metadata.gz: 2cc402f3ac1361b6eaa815bc97615c7d0754486b677fe930623e83a5c7b73f454dd348b6b9f3a942ff235d42e84512580f6ad29fd9af31757cace6f1094fb62c
7
- data.tar.gz: 497bec18e6a4a0b4d1f2fd1a7aeccb289877a2cd638d8a8297008d07fd86b0d87b276a3e193532a8160a4f774418dd4da51fd4ce497c58ea554673ef04af6385
6
+ metadata.gz: 5b88a3f310e277f889147d95801565704b294120a2d6dc0f9809ced9c214753c8638b3b50e5deeb94fb2c785d811fe1759de836246b41e7652352c7aa4b92532
7
+ data.tar.gz: b3f215424482e048dc8c24ef20262938d3f1808795573f91c9c4684529f3bd17b6992625d484106c5ebc40f92c258cdfe1a6319839e244926e7f10842300dd08
data/README.md CHANGED
@@ -23,6 +23,7 @@ end
23
23
  | Serviço | Métodos |
24
24
  |---|---|
25
25
  | `perata.checkouts` | `create` |
26
+ | `perata.customer_sessions` | `create` |
26
27
  | `perata.customers` | `create`, `get`, `list`, `list_iter` |
27
28
  | `perata.orders` | `list`, `list_iter` |
28
29
  | `perata.payment_methods` | `detach`, `list`, `list_iter` |
@@ -44,8 +45,15 @@ Uma recusa da API levanta `Perata::Error`, com `status`, `code` (`invalid_reques
44
45
 
45
46
  ```ruby
46
47
  event = Perata.verify_webhook(ENV.fetch("PERATA_WEBHOOK_SECRET"), request.headers, request.raw_post)
48
+ case event
49
+ when Perata::OrderWebhookEvent then puts event.type, event.data.id, event.data.installments
50
+ when Perata::DisputeWebhookEvent then puts event.data.provider_dispute_id
51
+ when Perata::UnknownWebhookEvent then nil # um tipo que esta versão ainda não conhece: event.data é o JSON
52
+ end
47
53
  ```
48
54
 
49
55
  A assinatura é a do Standard Webhooks (`webhook-id`, `webhook-timestamp`, `webhook-signature`), com tolerância de 5 minutos. Passe o corpo cru, antes de interpretar; os cabeçalhos podem vir num `Hash` ou no env do Rack. Uma assinatura que não confere levanta `Perata::WebhookVerificationError`.
50
56
 
51
- Versão do SDK: 0.1.1 · contrato da API: 0.1.0
57
+ 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
+
59
+ Versão do SDK: 0.2.0 · contrato da API: 0.1.0
data/lib/perata/client.rb CHANGED
@@ -32,6 +32,8 @@ module Perata
32
32
  class Client
33
33
  # @return [CheckoutsService]
34
34
  attr_reader :checkouts
35
+ # @return [CustomerSessionsService]
36
+ attr_reader :customer_sessions
35
37
  # @return [CustomersService]
36
38
  attr_reader :customers
37
39
  # @return [OrdersService]
@@ -55,6 +57,7 @@ module Perata
55
57
  @timeout = timeout
56
58
  @transport = transport
57
59
  @checkouts = CheckoutsService.new(self)
60
+ @customer_sessions = CustomerSessionsService.new(self)
58
61
  @customers = CustomersService.new(self)
59
62
  @orders = OrdersService.new(self)
60
63
  @payment_methods = PaymentMethodsService.new(self)
data/lib/perata/models.rb CHANGED
@@ -3,6 +3,71 @@
3
3
  # Code generated by sdk/generator. DO NOT EDIT.
4
4
 
5
5
  module Perata
6
+ # BenefitGrant is a resource of Peráta's API.
7
+ class BenefitGrant < Model
8
+ MEMBERS = %i[benefit_id benefit_type created_at customer_id granted_at id is_granted is_revoked revoked_at status].freeze
9
+
10
+ # @return [String]
11
+ attr_reader :benefit_id
12
+
13
+ # @return [String]
14
+ attr_reader :benefit_type
15
+
16
+ # @return [String]
17
+ attr_reader :created_at
18
+
19
+ # @return [String]
20
+ attr_reader :customer_id
21
+
22
+ # @return [String]
23
+ attr_reader :granted_at
24
+
25
+ # @return [String]
26
+ attr_reader :id
27
+
28
+ # @return [Boolean]
29
+ attr_reader :is_granted
30
+
31
+ # @return [Boolean]
32
+ attr_reader :is_revoked
33
+
34
+ # @return [String, nil]
35
+ attr_reader :revoked_at
36
+
37
+ # @return [String ("granted", "revoked")]
38
+ attr_reader :status
39
+
40
+ def initialize(benefit_id:, benefit_type:, created_at:, customer_id:, granted_at:, id:, is_granted:, is_revoked:, revoked_at:, status:)
41
+ @benefit_id = benefit_id
42
+ @benefit_type = benefit_type
43
+ @created_at = created_at
44
+ @customer_id = customer_id
45
+ @granted_at = granted_at
46
+ @id = id
47
+ @is_granted = is_granted
48
+ @is_revoked = is_revoked
49
+ @revoked_at = revoked_at
50
+ @status = status
51
+ freeze
52
+ end
53
+
54
+ # Builds a BenefitGrant from the API's JSON.
55
+ def self.from_hash(data)
56
+ new(
57
+ benefit_id: data.fetch("benefit_id"),
58
+ benefit_type: data.fetch("benefit_type"),
59
+ created_at: data.fetch("created_at"),
60
+ customer_id: data.fetch("customer_id"),
61
+ granted_at: data.fetch("granted_at"),
62
+ id: data.fetch("id"),
63
+ is_granted: data.fetch("is_granted"),
64
+ is_revoked: data.fetch("is_revoked"),
65
+ revoked_at: data["revoked_at"],
66
+ status: data.fetch("status")
67
+ )
68
+ end
69
+ end
70
+
6
71
  # Card is a resource of Peráta's API.
7
72
  class Card < Model
8
73
  MEMBERS = %i[brand exp_month exp_year last4].freeze
@@ -119,9 +184,41 @@ module Perata
119
184
  end
120
185
  end
121
186
 
187
+ # CustomerSession is a resource of Peráta's API.
188
+ class CustomerSession < Model
189
+ MEMBERS = %i[customer_portal_url expires_at token].freeze
190
+
191
+ # Endereço que abre o portal do cliente já autenticado.
192
+ # @return [String]
193
+ attr_reader :customer_portal_url
194
+
195
+ # @return [String]
196
+ attr_reader :expires_at
197
+
198
+ # Sessão do portal do cliente, já autenticada.
199
+ # @return [String]
200
+ attr_reader :token
201
+
202
+ def initialize(customer_portal_url:, expires_at:, token:)
203
+ @customer_portal_url = customer_portal_url
204
+ @expires_at = expires_at
205
+ @token = token
206
+ freeze
207
+ end
208
+
209
+ # Builds a CustomerSession from the API's JSON.
210
+ def self.from_hash(data)
211
+ new(
212
+ customer_portal_url: data.fetch("customer_portal_url"),
213
+ expires_at: data.fetch("expires_at"),
214
+ token: data.fetch("token")
215
+ )
216
+ end
217
+ end
218
+
122
219
  # Order is a resource of Peráta's API.
123
220
  class Order < Model
124
- MEMBERS = %i[amount_cents currency customer_id id paid_at price_id product_id status].freeze
221
+ MEMBERS = %i[amount_cents currency customer_id id installment_interest_cents installments paid_at price_id product_id status].freeze
125
222
 
126
223
  # @return [Integer]
127
224
  attr_reader :amount_cents
@@ -135,6 +232,14 @@ module Perata
135
232
  # @return [String]
136
233
  attr_reader :id
137
234
 
235
+ # Juros do parcelamento pagos pelo comprador, cobrados além de amount_cents.
236
+ # @return [Integer]
237
+ attr_reader :installment_interest_cents
238
+
239
+ # Em quantas parcelas o cartão de crédito foi cobrado; 1 à vista.
240
+ # @return [Integer]
241
+ attr_reader :installments
242
+
138
243
  # @return [String, nil]
139
244
  attr_reader :paid_at
140
245
 
@@ -147,11 +252,13 @@ module Perata
147
252
  # @return [String ("open", "confirmed", "succeeded", "failed", "expired")]
148
253
  attr_reader :status
149
254
 
150
- def initialize(amount_cents:, currency:, customer_id:, id:, paid_at:, price_id:, product_id:, status:)
255
+ def initialize(amount_cents:, currency:, customer_id:, id:, installment_interest_cents:, installments:, paid_at:, price_id:, product_id:, status:)
151
256
  @amount_cents = amount_cents
152
257
  @currency = currency
153
258
  @customer_id = customer_id
154
259
  @id = id
260
+ @installment_interest_cents = installment_interest_cents
261
+ @installments = installments
155
262
  @paid_at = paid_at
156
263
  @price_id = price_id
157
264
  @product_id = product_id
@@ -166,6 +273,8 @@ module Perata
166
273
  currency: data.fetch("currency"),
167
274
  customer_id: data.fetch("customer_id"),
168
275
  id: data.fetch("id"),
276
+ installment_interest_cents: data.fetch("installment_interest_cents"),
277
+ installments: data.fetch("installments"),
169
278
  paid_at: data["paid_at"],
170
279
  price_id: data.fetch("price_id"),
171
280
  product_id: data.fetch("product_id"),
@@ -201,7 +310,7 @@ module Perata
201
310
 
202
311
  # Payment is a resource of Peráta's API.
203
312
  class Payment < Model
204
- MEMBERS = %i[amount_cents captured_cents checkout_id created_at currency customer_id id method refunded_cents state].freeze
313
+ MEMBERS = %i[amount_cents captured_cents checkout_id created_at currency customer_id id installment_amount_cents installment_interest_cents installments method refunded_cents state].freeze
205
314
 
206
315
  # @return [Integer]
207
316
  attr_reader :amount_cents
@@ -224,6 +333,18 @@ module Perata
224
333
  # @return [String]
225
334
  attr_reader :id
226
335
 
336
+ # Valor de uma parcela; 0 à vista.
337
+ # @return [Integer]
338
+ attr_reader :installment_amount_cents
339
+
340
+ # Juros do parcelamento dentro de amount_cents.
341
+ # @return [Integer]
342
+ attr_reader :installment_interest_cents
343
+
344
+ # Em quantas parcelas o cartão de crédito foi cobrado; 1 à vista.
345
+ # @return [Integer]
346
+ attr_reader :installments
347
+
227
348
  # @return [String ("card", "pix", "boleto")]
228
349
  attr_reader :method
229
350
 
@@ -233,7 +354,7 @@ module Perata
233
354
  # @return [String ("unknown", "pending", "authorized", "captured", "refused", "canceled", "refunded", "partially_refunded")]
234
355
  attr_reader :state
235
356
 
236
- def initialize(amount_cents:, captured_cents:, checkout_id:, created_at:, currency:, customer_id:, id:, method:, refunded_cents:, state:)
357
+ def initialize(amount_cents:, captured_cents:, checkout_id:, created_at:, currency:, customer_id:, id:, installment_amount_cents:, installment_interest_cents:, installments:, method:, refunded_cents:, state:)
237
358
  @amount_cents = amount_cents
238
359
  @captured_cents = captured_cents
239
360
  @checkout_id = checkout_id
@@ -241,6 +362,9 @@ module Perata
241
362
  @currency = currency
242
363
  @customer_id = customer_id
243
364
  @id = id
365
+ @installment_amount_cents = installment_amount_cents
366
+ @installment_interest_cents = installment_interest_cents
367
+ @installments = installments
244
368
  @method = method
245
369
  @refunded_cents = refunded_cents
246
370
  @state = state
@@ -257,6 +381,9 @@ module Perata
257
381
  currency: data.fetch("currency"),
258
382
  customer_id: data.fetch("customer_id"),
259
383
  id: data.fetch("id"),
384
+ installment_amount_cents: data.fetch("installment_amount_cents"),
385
+ installment_interest_cents: data.fetch("installment_interest_cents"),
386
+ installments: data.fetch("installments"),
260
387
  method: data.fetch("method"),
261
388
  refunded_cents: data.fetch("refunded_cents"),
262
389
  state: data.fetch("state")
@@ -524,4 +651,171 @@ module Perata
524
651
  )
525
652
  end
526
653
  end
654
+
655
+ # WebhookCheckout is a resource of Peráta's API.
656
+ class WebhookCheckout < Model
657
+ MEMBERS = %i[amount_cents checkout_link_id created_at currency customer_id discount_cents expires_at id price_id product_id status total_cents].freeze
658
+
659
+ # @return [Integer]
660
+ attr_reader :amount_cents
661
+
662
+ # @return [String, nil]
663
+ attr_reader :checkout_link_id
664
+
665
+ # @return [String]
666
+ attr_reader :created_at
667
+
668
+ # @return [String ("BRL")]
669
+ attr_reader :currency
670
+
671
+ # Nulo até o comprador confirmar.
672
+ # @return [String, nil]
673
+ attr_reader :customer_id
674
+
675
+ # @return [Integer]
676
+ attr_reader :discount_cents
677
+
678
+ # @return [String]
679
+ attr_reader :expires_at
680
+
681
+ # @return [String]
682
+ attr_reader :id
683
+
684
+ # @return [String]
685
+ attr_reader :price_id
686
+
687
+ # @return [String]
688
+ attr_reader :product_id
689
+
690
+ # @return [String ("open", "confirmed", "succeeded", "failed", "expired")]
691
+ attr_reader :status
692
+
693
+ # @return [Integer]
694
+ attr_reader :total_cents
695
+
696
+ def initialize(amount_cents:, checkout_link_id:, created_at:, currency:, customer_id:, discount_cents:, expires_at:, id:, price_id:, product_id:, status:, total_cents:)
697
+ @amount_cents = amount_cents
698
+ @checkout_link_id = checkout_link_id
699
+ @created_at = created_at
700
+ @currency = currency
701
+ @customer_id = customer_id
702
+ @discount_cents = discount_cents
703
+ @expires_at = expires_at
704
+ @id = id
705
+ @price_id = price_id
706
+ @product_id = product_id
707
+ @status = status
708
+ @total_cents = total_cents
709
+ freeze
710
+ end
711
+
712
+ # Builds a WebhookCheckout from the API's JSON.
713
+ def self.from_hash(data)
714
+ new(
715
+ amount_cents: data.fetch("amount_cents"),
716
+ checkout_link_id: data["checkout_link_id"],
717
+ created_at: data.fetch("created_at"),
718
+ currency: data.fetch("currency"),
719
+ customer_id: data["customer_id"],
720
+ discount_cents: data.fetch("discount_cents"),
721
+ expires_at: data.fetch("expires_at"),
722
+ id: data.fetch("id"),
723
+ price_id: data.fetch("price_id"),
724
+ product_id: data.fetch("product_id"),
725
+ status: data.fetch("status"),
726
+ total_cents: data.fetch("total_cents")
727
+ )
728
+ end
729
+ end
730
+
731
+ # WebhookDispute is a resource of Peráta's API.
732
+ class WebhookDispute < Model
733
+ MEMBERS = %i[amount_cents currency customer_id decided_at due_by id invoice_id opened_at payment_id provider_dispute_id provider_reason reason source status].freeze
734
+
735
+ # Valor contestado, em centavos.
736
+ # @return [Integer]
737
+ attr_reader :amount_cents
738
+
739
+ # @return [String ("BRL")]
740
+ attr_reader :currency
741
+
742
+ # @return [String, nil]
743
+ attr_reader :customer_id
744
+
745
+ # @return [String, nil]
746
+ attr_reader :decided_at
747
+
748
+ # Prazo da defesa.
749
+ # @return [String, nil]
750
+ attr_reader :due_by
751
+
752
+ # @return [String]
753
+ attr_reader :id
754
+
755
+ # @return [String, nil]
756
+ attr_reader :invoice_id
757
+
758
+ # @return [String]
759
+ attr_reader :opened_at
760
+
761
+ # O PaymentId da venda contestada na Cielo.
762
+ # @return [String]
763
+ attr_reader :payment_id
764
+
765
+ # O número do caso na Cielo.
766
+ # @return [String]
767
+ attr_reader :provider_dispute_id
768
+
769
+ # O motivo nas palavras da Cielo.
770
+ # @return [String]
771
+ attr_reader :provider_reason
772
+
773
+ # Motivo, do conjunto fechado; unknown quando a adquirente não informou.
774
+ # @return [String]
775
+ attr_reader :reason
776
+
777
+ # @return [String ("operator", "cielo_chargeback")]
778
+ attr_reader :source
779
+
780
+ # @return [String ("opened", "needs_evidence", "submitted", "under_review", "won", "lost", "closed")]
781
+ attr_reader :status
782
+
783
+ def initialize(amount_cents:, currency:, customer_id:, decided_at:, due_by:, id:, invoice_id:, opened_at:, payment_id:, provider_dispute_id:, provider_reason:, reason:, source:, status:)
784
+ @amount_cents = amount_cents
785
+ @currency = currency
786
+ @customer_id = customer_id
787
+ @decided_at = decided_at
788
+ @due_by = due_by
789
+ @id = id
790
+ @invoice_id = invoice_id
791
+ @opened_at = opened_at
792
+ @payment_id = payment_id
793
+ @provider_dispute_id = provider_dispute_id
794
+ @provider_reason = provider_reason
795
+ @reason = reason
796
+ @source = source
797
+ @status = status
798
+ freeze
799
+ end
800
+
801
+ # Builds a WebhookDispute from the API's JSON.
802
+ def self.from_hash(data)
803
+ new(
804
+ amount_cents: data.fetch("amount_cents"),
805
+ currency: data.fetch("currency"),
806
+ customer_id: data["customer_id"],
807
+ decided_at: data["decided_at"],
808
+ due_by: data["due_by"],
809
+ id: data.fetch("id"),
810
+ invoice_id: data["invoice_id"],
811
+ opened_at: data.fetch("opened_at"),
812
+ payment_id: data.fetch("payment_id"),
813
+ provider_dispute_id: data.fetch("provider_dispute_id"),
814
+ provider_reason: data.fetch("provider_reason"),
815
+ reason: data.fetch("reason"),
816
+ source: data.fetch("source"),
817
+ status: data.fetch("status")
818
+ )
819
+ end
820
+ end
527
821
  end
data/lib/perata/params.rb CHANGED
@@ -48,6 +48,26 @@ module Perata
48
48
  end
49
49
  end
50
50
 
51
+ # The body of customer-sessions:create.
52
+ class CustomerSessionsCreateParams < Params
53
+ MEMBERS = %i[customer_id].freeze
54
+
55
+ # @return [String]
56
+ attr_reader :customer_id
57
+
58
+ def initialize(customer_id:)
59
+ @customer_id = customer_id
60
+ freeze
61
+ end
62
+
63
+ # The body as the API reads it.
64
+ def to_wire
65
+ wire = {}
66
+ wire["customer_id"] = @customer_id
67
+ wire
68
+ end
69
+ end
70
+
51
71
  # The body of customers:create.
52
72
  class CustomersCreateParams < Params
53
73
  MEMBERS = %i[email external_id name].freeze
@@ -19,6 +19,22 @@ module Perata
19
19
  end
20
20
  end
21
21
 
22
+ # The API's customer-sessions.
23
+ class CustomerSessionsService
24
+ def initialize(client)
25
+ @client = client
26
+ end
27
+
28
+ # Sign a customer into the customer portal: the address opens it already signed in.
29
+ #
30
+ # @param params [CustomerSessionsCreateParams, Hash]
31
+ # @return [CustomerSession]
32
+ def create(params, idempotency_key: nil, api_key: nil)
33
+ data = @client.request("POST", "/api/v1/customer-sessions", body: CustomerSessionsCreateParams.coerce(params).to_wire, idempotency_key: idempotency_key, api_key: api_key)
34
+ CustomerSession.from_hash(data)
35
+ end
36
+ end
37
+
22
38
  # The API's customers.
23
39
  class CustomersService
24
40
  def initialize(client)
@@ -4,7 +4,7 @@
4
4
 
5
5
  module Perata
6
6
  # This SDK's version.
7
- VERSION = "0.1.1"
7
+ VERSION = "0.2.0"
8
8
 
9
9
  # The contract this SDK was generated from.
10
10
  API_VERSION = "0.1.0"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Code generated by sdk/generator. DO NOT EDIT.
4
+
3
5
  require "json"
4
6
  require "openssl"
5
7
 
@@ -15,13 +17,19 @@ module Perata
15
17
  end
16
18
  end
17
19
 
18
- # What Peráta posts to an endpoint.
20
+ # A webhook Peráta sent, verified: one subclass a kind of data
21
+ # (OrderWebhookEvent carries an Order…), or UnknownWebhookEvent for a type
22
+ # this SDK does not know yet. Within a class, +type+ says what happened.
19
23
  class WebhookEvent < Model
20
24
  MEMBERS = %i[id type timestamp data].freeze
21
25
 
22
- # @return [String]
23
- attr_reader :id, :type, :timestamp
24
- # @return [Hash{String => String}]
26
+ # @return [String] the message's id, the same as the webhook-id header
27
+ attr_reader :id
28
+ # @return [String] what happened: order.paid…
29
+ attr_reader :type
30
+ # @return [String] when it happened, in RFC 3339
31
+ attr_reader :timestamp
32
+ # @return [Object] what the event carries
25
33
  attr_reader :data
26
34
 
27
35
  def initialize(id:, type:, timestamp:, data:)
@@ -33,16 +41,71 @@ module Perata
33
41
  end
34
42
  end
35
43
 
44
+ # The events that carry a BenefitGrant; +data+ is BenefitGrant.
45
+ class BenefitGrantWebhookEvent < WebhookEvent
46
+ TYPES = %w[benefit_grant.created benefit_grant.revoked benefit_grant.updated].freeze
47
+ end
48
+
49
+ # The events that carry a WebhookCheckout; +data+ is WebhookCheckout.
50
+ class CheckoutWebhookEvent < WebhookEvent
51
+ TYPES = %w[checkout.created checkout.expired checkout.updated].freeze
52
+ end
53
+
54
+ # The events that carry a Customer; +data+ is Customer.
55
+ class CustomerWebhookEvent < WebhookEvent
56
+ TYPES = %w[customer.created customer.deleted customer.updated].freeze
57
+ end
58
+
59
+ # The events that carry a WebhookDispute; +data+ is WebhookDispute.
60
+ class DisputeWebhookEvent < WebhookEvent
61
+ TYPES = %w[dispute.lost dispute.opened dispute.won].freeze
62
+ end
63
+
64
+ # The events that carry the event's identifiers; +data+ is Hash{String => String}.
65
+ class IdentifiersWebhookEvent < WebhookEvent
66
+ TYPES = %w[benefit.archived benefit.created benefit.updated benefit.version_created benefit_product_link.linked benefit_product_link.unlinked checkout_link.archived checkout_link.created checkout_payment.authorized checkout_payment.canceled checkout_payment.captured checkout_payment.partially_refunded checkout_payment.refunded checkout_payment.refused checkout_session.confirmed checkout_session.expired checkout_session.failed checkout_session.opened checkout_session.succeeded customer.archived discount.created discount.deleted discount.exhausted discount.expired discount.reactivated discount.redeemed discount.updated discount_redemption.released invoice.drafted invoice.issued invoice.settled invoice.uncollectible invoice.voided price.archived price.created product.archived product.created product.updated refund.canceled refund.failed refund.requested refund.succeeded seat_allocation.opened seat_allocation.resized seat_assignment.claimed seat_assignment.invited seat_assignment.revoked usage_event.recorded usage_period.closed usage_period.opened usage_period_adjustment.created].freeze
67
+ end
68
+
69
+ # The events that carry an Order; +data+ is Order.
70
+ class OrderWebhookEvent < WebhookEvent
71
+ TYPES = %w[order.created order.paid order.refunded order.updated].freeze
72
+ end
73
+
74
+ # The events that carry a Refund; +data+ is Refund.
75
+ class RefundWebhookEvent < WebhookEvent
76
+ TYPES = %w[refund.created refund.updated].freeze
77
+ end
78
+
79
+ # The events that carry a Subscription; +data+ is Subscription.
80
+ class SubscriptionWebhookEvent < WebhookEvent
81
+ TYPES = %w[subscription.active subscription.canceled subscription.created subscription.cycled subscription.past_due subscription.paused subscription.resumed subscription.revoked subscription.uncanceled subscription.updated].freeze
82
+ end
83
+
84
+ # An event of a type this SDK does not know yet, its data as it was sent.
85
+ class UnknownWebhookEvent < WebhookEvent
86
+ end
87
+
36
88
  # Checks a webhook Peráta sent and answers its event.
37
89
  #
38
90
  # The signature is an HMAC-SHA256 of +{webhook-id}.{webhook-timestamp}.{body}+
39
- # under the key the endpoint's +whsec_+ secret encodes. Pass the body exactly
40
- # as it arrived, before parsing it; +headers+ is a hash of the request's
41
- # headers, in any case, or a Rack env.
91
+ # under the key the endpoint's +whsec_+ secret encodes, one of the signatures
92
+ # in the +webhook-signature+ header, which carries more than one while a
93
+ # secret is rotated. Pass the body exactly as it arrived, before parsing it;
94
+ # +headers+ is a hash of the request's headers, in any case, or a Rack env.
42
95
  #
43
96
  # @return [WebhookEvent]
44
97
  # @raise [WebhookVerificationError]
45
98
  def self.verify_webhook(secret, headers, body, now: Time.now)
99
+ verify_webhook_signature(secret, headers, body, now: now)
100
+ webhook_event(body.to_s.b.force_encoding(Encoding::UTF_8))
101
+ end
102
+
103
+ # Checks a webhook's signature as verify_webhook does, without reading its
104
+ # body.
105
+ #
106
+ # @return [nil]
107
+ # @raise [WebhookVerificationError]
108
+ def self.verify_webhook_signature(secret, headers, body, now: Time.now)
46
109
  key = begin
47
110
  secret.delete_prefix("whsec_").unpack1("m0")
48
111
  rescue ArgumentError
@@ -66,15 +129,30 @@ module Perata
66
129
  rescue ArgumentError
67
130
  next
68
131
  end
69
- next unless decoded.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(decoded, expected)
70
-
71
- event = JSON.parse(raw.force_encoding(Encoding::UTF_8))
72
- return WebhookEvent.new(id: event.fetch("id"), type: event.fetch("type"), timestamp: event.fetch("timestamp"),
73
- data: event["data"] || {})
132
+ return nil if decoded.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(decoded, expected)
74
133
  end
75
134
  raise WebhookVerificationError
76
135
  end
77
136
 
137
+ # A verified body as the event its type names.
138
+ def self.webhook_event(raw)
139
+ event = JSON.parse(raw)
140
+ data = event["data"] || {}
141
+ message = { id: event.fetch("id"), type: event.fetch("type"), timestamp: event.fetch("timestamp") }
142
+ case message[:type]
143
+ when *BenefitGrantWebhookEvent::TYPES then BenefitGrantWebhookEvent.new(**message, data: BenefitGrant.from_hash(data))
144
+ when *CheckoutWebhookEvent::TYPES then CheckoutWebhookEvent.new(**message, data: WebhookCheckout.from_hash(data))
145
+ when *CustomerWebhookEvent::TYPES then CustomerWebhookEvent.new(**message, data: Customer.from_hash(data))
146
+ when *DisputeWebhookEvent::TYPES then DisputeWebhookEvent.new(**message, data: WebhookDispute.from_hash(data))
147
+ when *IdentifiersWebhookEvent::TYPES then IdentifiersWebhookEvent.new(**message, data: data)
148
+ when *OrderWebhookEvent::TYPES then OrderWebhookEvent.new(**message, data: Order.from_hash(data))
149
+ when *RefundWebhookEvent::TYPES then RefundWebhookEvent.new(**message, data: Refund.from_hash(data))
150
+ when *SubscriptionWebhookEvent::TYPES then SubscriptionWebhookEvent.new(**message, data: Subscription.from_hash(data))
151
+ else UnknownWebhookEvent.new(**message, data: data)
152
+ end
153
+ end
154
+ private_class_method :webhook_event
155
+
78
156
  # A header's value, looked up in any case; a Rack env's HTTP_WEBHOOK_ID
79
157
  # names webhook-id.
80
158
  def self.webhook_header(headers, name)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: perata-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peráta
@@ -45,7 +45,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
45
45
  - !ruby/object:Gem::Version
46
46
  version: '0'
47
47
  requirements: []
48
- rubygems_version: 4.0.16
48
+ rubygems_version: 3.6.9
49
49
  specification_version: 4
50
50
  summary: 'Peráta''s SDK for Ruby: products, customers, checkouts, payments, subscriptions
51
51
  and webhooks.'