perata-sdk 0.1.1 → 0.3.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.
@@ -4,7 +4,7 @@
4
4
 
5
5
  module Perata
6
6
  # This SDK's version.
7
- VERSION = "0.1.1"
7
+ VERSION = "0.3.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,86 @@ module Perata
33
41
  end
34
42
  end
35
43
 
44
+ # The events that carry a WebhookBenefit; +data+ is WebhookBenefit.
45
+ class BenefitWebhookEvent < WebhookEvent
46
+ TYPES = %w[benefit.created benefit.updated].freeze
47
+ end
48
+
49
+ # The events that carry a BenefitGrant; +data+ is BenefitGrant.
50
+ class BenefitGrantWebhookEvent < WebhookEvent
51
+ TYPES = %w[benefit_grant.created benefit_grant.revoked benefit_grant.updated].freeze
52
+ end
53
+
54
+ # The events that carry a WebhookCheckout; +data+ is WebhookCheckout.
55
+ class CheckoutWebhookEvent < WebhookEvent
56
+ TYPES = %w[checkout.created checkout.expired checkout.updated].freeze
57
+ end
58
+
59
+ # The events that carry a Customer; +data+ is Customer.
60
+ class CustomerWebhookEvent < WebhookEvent
61
+ TYPES = %w[customer.created customer.deleted customer.updated].freeze
62
+ end
63
+
64
+ # The events that carry a WebhookDiscount; +data+ is WebhookDiscount.
65
+ class DiscountWebhookEvent < WebhookEvent
66
+ TYPES = %w[discount.created discount.deleted discount.updated].freeze
67
+ end
68
+
69
+ # The events that carry a WebhookDispute; +data+ is WebhookDispute.
70
+ class DisputeWebhookEvent < WebhookEvent
71
+ TYPES = %w[dispute.lost dispute.opened dispute.won].freeze
72
+ end
73
+
74
+ # The events that carry the event's identifiers; +data+ is Hash{String => String}.
75
+ class IdentifiersWebhookEvent < WebhookEvent
76
+ TYPES = %w[benefit.archived 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.exhausted discount.expired discount.reactivated discount.redeemed discount_redemption.released invoice.drafted invoice.issued invoice.settled invoice.uncollectible invoice.voided price.archived price.created product.archived 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
77
+ end
78
+
79
+ # The events that carry an Order; +data+ is Order.
80
+ class OrderWebhookEvent < WebhookEvent
81
+ TYPES = %w[order.created order.paid order.refunded order.updated].freeze
82
+ end
83
+
84
+ # The events that carry a Product; +data+ is Product.
85
+ class ProductWebhookEvent < WebhookEvent
86
+ TYPES = %w[product.created product.updated].freeze
87
+ end
88
+
89
+ # The events that carry a Refund; +data+ is Refund.
90
+ class RefundWebhookEvent < WebhookEvent
91
+ TYPES = %w[refund.created refund.updated].freeze
92
+ end
93
+
94
+ # The events that carry a Subscription; +data+ is Subscription.
95
+ class SubscriptionWebhookEvent < WebhookEvent
96
+ TYPES = %w[subscription.active subscription.canceled subscription.created subscription.cycled subscription.past_due subscription.paused subscription.resumed subscription.revoked subscription.uncanceled subscription.updated].freeze
97
+ end
98
+
99
+ # An event of a type this SDK does not know yet, its data as it was sent.
100
+ class UnknownWebhookEvent < WebhookEvent
101
+ end
102
+
36
103
  # Checks a webhook Peráta sent and answers its event.
37
104
  #
38
105
  # 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.
106
+ # under the key the endpoint's +whsec_+ secret encodes, one of the signatures
107
+ # in the +webhook-signature+ header, which carries more than one while a
108
+ # secret is rotated. Pass the body exactly as it arrived, before parsing it;
109
+ # +headers+ is a hash of the request's headers, in any case, or a Rack env.
42
110
  #
43
111
  # @return [WebhookEvent]
44
112
  # @raise [WebhookVerificationError]
45
113
  def self.verify_webhook(secret, headers, body, now: Time.now)
114
+ verify_webhook_signature(secret, headers, body, now: now)
115
+ webhook_event(body.to_s.b.force_encoding(Encoding::UTF_8))
116
+ end
117
+
118
+ # Checks a webhook's signature as verify_webhook does, without reading its
119
+ # body.
120
+ #
121
+ # @return [nil]
122
+ # @raise [WebhookVerificationError]
123
+ def self.verify_webhook_signature(secret, headers, body, now: Time.now)
46
124
  key = begin
47
125
  secret.delete_prefix("whsec_").unpack1("m0")
48
126
  rescue ArgumentError
@@ -66,15 +144,33 @@ module Perata
66
144
  rescue ArgumentError
67
145
  next
68
146
  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"] || {})
147
+ return nil if decoded.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(decoded, expected)
74
148
  end
75
149
  raise WebhookVerificationError
76
150
  end
77
151
 
152
+ # A verified body as the event its type names.
153
+ def self.webhook_event(raw)
154
+ event = JSON.parse(raw)
155
+ data = event["data"] || {}
156
+ message = { id: event.fetch("id"), type: event.fetch("type"), timestamp: event.fetch("timestamp") }
157
+ case message[:type]
158
+ when *BenefitWebhookEvent::TYPES then BenefitWebhookEvent.new(**message, data: WebhookBenefit.from_hash(data))
159
+ when *BenefitGrantWebhookEvent::TYPES then BenefitGrantWebhookEvent.new(**message, data: BenefitGrant.from_hash(data))
160
+ when *CheckoutWebhookEvent::TYPES then CheckoutWebhookEvent.new(**message, data: WebhookCheckout.from_hash(data))
161
+ when *CustomerWebhookEvent::TYPES then CustomerWebhookEvent.new(**message, data: Customer.from_hash(data))
162
+ when *DiscountWebhookEvent::TYPES then DiscountWebhookEvent.new(**message, data: WebhookDiscount.from_hash(data))
163
+ when *DisputeWebhookEvent::TYPES then DisputeWebhookEvent.new(**message, data: WebhookDispute.from_hash(data))
164
+ when *IdentifiersWebhookEvent::TYPES then IdentifiersWebhookEvent.new(**message, data: data)
165
+ when *OrderWebhookEvent::TYPES then OrderWebhookEvent.new(**message, data: Order.from_hash(data))
166
+ when *ProductWebhookEvent::TYPES then ProductWebhookEvent.new(**message, data: Product.from_hash(data))
167
+ when *RefundWebhookEvent::TYPES then RefundWebhookEvent.new(**message, data: Refund.from_hash(data))
168
+ when *SubscriptionWebhookEvent::TYPES then SubscriptionWebhookEvent.new(**message, data: Subscription.from_hash(data))
169
+ else UnknownWebhookEvent.new(**message, data: data)
170
+ end
171
+ end
172
+ private_class_method :webhook_event
173
+
78
174
  # A header's value, looked up in any case; a Rack env's HTTP_WEBHOOK_ID
79
175
  # names webhook-id.
80
176
  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.3.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.'