Module: Invoq::Webhooks

Defined in:
lib/invoq/webhooks.rb

Constant Summary collapse

DEFAULT_TOLERANCE_SECONDS =
300
SIGNATURE_PATTERN =
/\A[a-f0-9]{64}\z/i
MISSING =
Object.new

Class Method Summary collapse

Class Method Details

.invoice_paid?(event) ⇒ Boolean

Returns:

  • (Boolean)


58
59
60
61
62
63
64
# File 'lib/invoq/webhooks.rb', line 58

def self.invoice_paid?(event)
  invoice = lifecycle_event_invoice(event, "invoice.paid")

  # Paid-equivalent statuses only: review_required has money against it but
  # is not cleared for fulfillment.
  !invoice.nil? && invoice_paid_status?(invoice["status"])
end

.invoice_payment_reversed?(event) ⇒ Boolean

Returns:

  • (Boolean)


70
71
72
73
74
75
# File 'lib/invoq/webhooks.rb', line 70

def self.invoice_payment_reversed?(event)
  # No status check, unlike the paid guard: rejecting an unrecognized status
  # would drop the event and leave the order fulfilled on a payment that no
  # longer exists.
  !lifecycle_event_invoice(event, "invoice.payment_reversed").nil?
end

.is_invoice_paid(event) ⇒ Object



66
67
68
# File 'lib/invoq/webhooks.rb', line 66

def self.is_invoice_paid(event)
  invoice_paid?(event)
end

.is_invoice_payment_reversed(event) ⇒ Object



77
78
79
# File 'lib/invoq/webhooks.rb', line 77

def self.is_invoice_payment_reversed(event)
  invoice_payment_reversed?(event)
end

.verify_webhook(raw_body, headers, webhook_secret) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/invoq/webhooks.rb', line 14

def self.verify_webhook(raw_body, headers, webhook_secret)
  signature_header = get_signature_header(headers)

  if signature_header.nil? || signature_header.empty?
    raise signature_error("missing_signature", "Missing invoq-signature header.")
  end

  unless webhook_secret.is_a?(String) && !webhook_secret.empty?
    raise signature_error(
      "invalid_signature_header",
      "Webhook secret must be a non-empty string."
    )
  end

  timestamp, timestamp_seconds, signature = parse_signature_header(signature_header)
  now_seconds = Time.now.to_i

  if (now_seconds - timestamp_seconds).abs > DEFAULT_TOLERANCE_SECONDS
    raise signature_error(
      "timestamp_outside_tolerance",
      "Webhook timestamp is outside the allowed tolerance."
    )
  end

  expected_signature = hmac_sha256_hex(webhook_secret, timestamp, raw_body)

  unless constant_time_equal?(expected_signature, signature)
    raise signature_error("signature_mismatch", "Webhook signature mismatch.")
  end

  payload = JSON.parse(body_text(raw_body))

  unless payload.is_a?(Hash) && payload["type"].is_a?(String)
    raise signature_error(
      "invalid_payload",
      "Webhook payload must be an object with a string type."
    )
  end

  payload
rescue JSON::ParserError
  raise signature_error("invalid_payload", "Webhook payload is not valid JSON.")
end