stripe 4.24.0 → 5.0.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.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +17 -4
  3. data/.rubocop_todo.yml +10 -9
  4. data/.travis.yml +1 -5
  5. data/CHANGELOG.md +22 -0
  6. data/Gemfile +2 -12
  7. data/README.md +10 -10
  8. data/Rakefile +8 -7
  9. data/VERSION +1 -1
  10. data/lib/stripe.rb +56 -15
  11. data/lib/stripe/api_operations/list.rb +0 -6
  12. data/lib/stripe/connection_manager.rb +131 -0
  13. data/lib/stripe/error_object.rb +94 -0
  14. data/lib/stripe/errors.rb +15 -2
  15. data/lib/stripe/list_object.rb +2 -1
  16. data/lib/stripe/multipart_encoder.rb +131 -0
  17. data/lib/stripe/object_types.rb +0 -1
  18. data/lib/stripe/resources.rb +0 -1
  19. data/lib/stripe/resources/account.rb +1 -5
  20. data/lib/stripe/resources/account_link.rb +1 -1
  21. data/lib/stripe/resources/alipay_account.rb +1 -1
  22. data/lib/stripe/resources/apple_pay_domain.rb +1 -1
  23. data/lib/stripe/resources/application_fee.rb +1 -12
  24. data/lib/stripe/resources/application_fee_refund.rb +1 -1
  25. data/lib/stripe/resources/balance.rb +1 -1
  26. data/lib/stripe/resources/balance_transaction.rb +1 -1
  27. data/lib/stripe/resources/bank_account.rb +1 -1
  28. data/lib/stripe/resources/bitcoin_receiver.rb +1 -1
  29. data/lib/stripe/resources/bitcoin_transaction.rb +1 -1
  30. data/lib/stripe/resources/capability.rb +1 -1
  31. data/lib/stripe/resources/card.rb +1 -1
  32. data/lib/stripe/resources/charge.rb +7 -69
  33. data/lib/stripe/resources/checkout/session.rb +1 -1
  34. data/lib/stripe/resources/country_spec.rb +1 -1
  35. data/lib/stripe/resources/coupon.rb +1 -1
  36. data/lib/stripe/resources/credit_note.rb +1 -1
  37. data/lib/stripe/resources/customer.rb +3 -63
  38. data/lib/stripe/resources/customer_balance_transaction.rb +1 -1
  39. data/lib/stripe/resources/discount.rb +1 -1
  40. data/lib/stripe/resources/dispute.rb +1 -7
  41. data/lib/stripe/resources/ephemeral_key.rb +1 -1
  42. data/lib/stripe/resources/event.rb +1 -1
  43. data/lib/stripe/resources/exchange_rate.rb +1 -1
  44. data/lib/stripe/resources/file.rb +3 -13
  45. data/lib/stripe/resources/file_link.rb +1 -1
  46. data/lib/stripe/resources/invoice.rb +6 -1
  47. data/lib/stripe/resources/invoice_item.rb +1 -1
  48. data/lib/stripe/resources/invoice_line_item.rb +1 -1
  49. data/lib/stripe/resources/issuing/authorization.rb +1 -1
  50. data/lib/stripe/resources/issuing/card.rb +1 -1
  51. data/lib/stripe/resources/issuing/card_details.rb +1 -1
  52. data/lib/stripe/resources/issuing/cardholder.rb +1 -1
  53. data/lib/stripe/resources/issuing/dispute.rb +1 -1
  54. data/lib/stripe/resources/issuing/transaction.rb +1 -1
  55. data/lib/stripe/resources/login_link.rb +1 -1
  56. data/lib/stripe/resources/order.rb +1 -9
  57. data/lib/stripe/resources/order_return.rb +1 -1
  58. data/lib/stripe/resources/payment_intent.rb +1 -1
  59. data/lib/stripe/resources/payment_method.rb +1 -1
  60. data/lib/stripe/resources/payout.rb +1 -7
  61. data/lib/stripe/resources/person.rb +1 -1
  62. data/lib/stripe/resources/plan.rb +1 -1
  63. data/lib/stripe/resources/product.rb +1 -1
  64. data/lib/stripe/resources/radar/early_fraud_warning.rb +1 -1
  65. data/lib/stripe/resources/radar/value_list.rb +1 -1
  66. data/lib/stripe/resources/radar/value_list_item.rb +1 -1
  67. data/lib/stripe/resources/recipient.rb +1 -5
  68. data/lib/stripe/resources/recipient_transfer.rb +1 -1
  69. data/lib/stripe/resources/refund.rb +1 -1
  70. data/lib/stripe/resources/reporting/report_run.rb +1 -1
  71. data/lib/stripe/resources/reporting/report_type.rb +1 -1
  72. data/lib/stripe/resources/reversal.rb +1 -1
  73. data/lib/stripe/resources/review.rb +1 -1
  74. data/lib/stripe/resources/setup_intent.rb +1 -1
  75. data/lib/stripe/resources/sigma/scheduled_query_run.rb +1 -1
  76. data/lib/stripe/resources/sku.rb +1 -1
  77. data/lib/stripe/resources/source.rb +1 -7
  78. data/lib/stripe/resources/source_transaction.rb +1 -1
  79. data/lib/stripe/resources/subscription.rb +9 -9
  80. data/lib/stripe/resources/subscription_item.rb +1 -1
  81. data/lib/stripe/resources/subscription_schedule.rb +1 -1
  82. data/lib/stripe/resources/tax_id.rb +1 -1
  83. data/lib/stripe/resources/tax_rate.rb +1 -1
  84. data/lib/stripe/resources/terminal/connection_token.rb +1 -1
  85. data/lib/stripe/resources/terminal/location.rb +1 -1
  86. data/lib/stripe/resources/terminal/reader.rb +1 -1
  87. data/lib/stripe/resources/three_d_secure.rb +1 -1
  88. data/lib/stripe/resources/token.rb +1 -1
  89. data/lib/stripe/resources/topup.rb +1 -1
  90. data/lib/stripe/resources/transfer.rb +1 -6
  91. data/lib/stripe/resources/usage_record.rb +1 -17
  92. data/lib/stripe/resources/usage_record_summary.rb +1 -1
  93. data/lib/stripe/resources/webhook_endpoint.rb +1 -1
  94. data/lib/stripe/stripe_client.rb +281 -183
  95. data/lib/stripe/stripe_object.rb +4 -23
  96. data/lib/stripe/stripe_response.rb +53 -21
  97. data/lib/stripe/util.rb +10 -11
  98. data/lib/stripe/version.rb +1 -1
  99. data/lib/stripe/webhook.rb +1 -1
  100. data/stripe.gemspec +6 -9
  101. data/test/stripe/account_test.rb +0 -16
  102. data/test/stripe/api_operations_test.rb +2 -2
  103. data/test/stripe/api_resource_test.rb +2 -10
  104. data/test/stripe/charge_test.rb +0 -16
  105. data/test/stripe/connection_manager_test.rb +138 -0
  106. data/test/stripe/customer_test.rb +1 -44
  107. data/test/stripe/errors_test.rb +29 -8
  108. data/test/stripe/file_test.rb +0 -10
  109. data/test/stripe/invoice_test.rb +17 -1
  110. data/test/stripe/list_object_test.rb +0 -16
  111. data/test/stripe/login_link_test.rb +1 -1
  112. data/test/stripe/multipart_encoder_test.rb +130 -0
  113. data/test/stripe/payment_intent_test.rb +1 -1
  114. data/test/stripe/setup_intent_test.rb +1 -1
  115. data/test/stripe/source_test.rb +0 -18
  116. data/test/stripe/stripe_client_test.rb +214 -29
  117. data/test/stripe/stripe_object_test.rb +7 -35
  118. data/test/stripe/stripe_response_test.rb +70 -24
  119. data/test/stripe/subscription_test.rb +2 -2
  120. data/test/stripe/webhook_test.rb +2 -2
  121. data/test/stripe_mock.rb +4 -3
  122. data/test/stripe_test.rb +0 -13
  123. data/test/test_helper.rb +10 -5
  124. metadata +11 -39
  125. data/lib/stripe/resources/issuer_fraud_record.rb +0 -9
  126. data/test/stripe/file_upload_test.rb +0 -79
  127. data/test/stripe/issuer_fraud_record_test.rb +0 -20
  128. data/test/stripe/usage_record_test.rb +0 -28
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stripe
4
+ # Represents an error object as returned by the API.
5
+ #
6
+ # @see https://stripe.com/docs/api/errors
7
+ class ErrorObject < StripeObject
8
+ # Unlike other objects, we explicitly declare getter methods here. This
9
+ # is because the API doesn't return `null` values for fields on this
10
+ # object, rather the fields are omitted entirely. Not declaring the getter
11
+ # methods would cause users to run into `NoMethodError` exceptions and
12
+ # get in the way of generic error handling.
13
+
14
+ # For card errors, the ID of the failed charge.
15
+ def charge
16
+ @values[:charge]
17
+ end
18
+
19
+ # For some errors that could be handled programmatically, a short string
20
+ # indicating the error code reported.
21
+ def code
22
+ @values[:code]
23
+ end
24
+
25
+ # For card errors resulting from a card issuer decline, a short string
26
+ # indicating the card issuer's reason for the decline if they provide one.
27
+ def decline_code
28
+ @values[:decline_code]
29
+ end
30
+
31
+ # A URL to more information about the error code reported.
32
+ def doc_url
33
+ @values[:doc_url]
34
+ end
35
+
36
+ # A human-readable message providing more details about the error. For card
37
+ # errors, these messages can be shown to your users.
38
+ def message
39
+ @values[:message]
40
+ end
41
+
42
+ # If the error is parameter-specific, the parameter related to the error.
43
+ # For example, you can use this to display a message near the correct form
44
+ # field.
45
+ def param
46
+ @values[:param]
47
+ end
48
+
49
+ # The PaymentIntent object for errors returned on a request involving a
50
+ # PaymentIntent.
51
+ def payment_intent
52
+ @values[:payment_intent]
53
+ end
54
+
55
+ # The PaymentMethod object for errors returned on a request involving a
56
+ # PaymentMethod.
57
+ def payment_method
58
+ @values[:payment_method]
59
+ end
60
+
61
+ # The SetupIntent object for errors returned on a request involving a
62
+ # SetupIntent.
63
+ def setup_intent
64
+ @values[:setup_intent]
65
+ end
66
+
67
+ # The source object for errors returned on a request involving a source.
68
+ def source
69
+ @values[:source]
70
+ end
71
+
72
+ # The type of error returned. One of `api_connection_error`, `api_error`,
73
+ # `authentication_error`, `card_error`, `idempotency_error`,
74
+ # `invalid_request_error`, or `rate_limit_error`.
75
+ def type
76
+ @values[:type]
77
+ end
78
+ end
79
+
80
+ # Represents on OAuth error returned by the OAuth API.
81
+ #
82
+ # @see https://stripe.com/docs/connect/oauth-reference#post-token-errors
83
+ class OAuthErrorObject < StripeObject
84
+ # A unique error code per error type.
85
+ def error
86
+ @values[:error]
87
+ end
88
+
89
+ # A human readable description of the error.
90
+ def error_description
91
+ @values[:error_description]
92
+ end
93
+ end
94
+ end
@@ -11,6 +11,7 @@ module Stripe
11
11
  attr_accessor :response
12
12
 
13
13
  attr_reader :code
14
+ attr_reader :error
14
15
  attr_reader :http_body
15
16
  attr_reader :http_headers
16
17
  attr_reader :http_status
@@ -27,6 +28,13 @@ module Stripe
27
28
  @json_body = json_body
28
29
  @code = code
29
30
  @request_id = @http_headers[:request_id]
31
+ @error = construct_error_object
32
+ end
33
+
34
+ def construct_error_object
35
+ return nil if @json_body.nil? || !@json_body.key?(:error)
36
+
37
+ ErrorObject.construct_from(@json_body[:error])
30
38
  end
31
39
 
32
40
  def to_s
@@ -59,8 +67,7 @@ module Stripe
59
67
  class CardError < StripeError
60
68
  attr_reader :param
61
69
 
62
- # TODO: make code a keyword arg in next major release
63
- def initialize(message, param, code, http_status: nil, http_body: nil,
70
+ def initialize(message, param, code: nil, http_status: nil, http_body: nil,
64
71
  json_body: nil, http_headers: nil)
65
72
  super(message, http_status: http_status, http_body: http_body,
66
73
  json_body: json_body, http_headers: http_headers,
@@ -119,6 +126,12 @@ module Stripe
119
126
  json_body: json_body, http_headers: http_headers,
120
127
  code: code)
121
128
  end
129
+
130
+ def construct_error_object
131
+ return nil if @json_body.nil?
132
+
133
+ OAuthErrorObject.construct_from(@json_body)
134
+ end
122
135
  end
123
136
 
124
137
  # InvalidClientError is raised when the client doesn't belong to you, or
@@ -7,7 +7,7 @@ module Stripe
7
7
  include Stripe::APIOperations::Request
8
8
  include Stripe::APIOperations::Create
9
9
 
10
- OBJECT_NAME = "list".freeze
10
+ OBJECT_NAME = "list"
11
11
 
12
12
  # This accessor allows a `ListObject` to inherit various filters that were
13
13
  # given to a predecessor. This allows for things like consistent limits,
@@ -83,6 +83,7 @@ module Stripe
83
83
  # was given, the default limit will be fetched again.
84
84
  def next_page(params = {}, opts = {})
85
85
  return self.class.empty_list(opts) unless has_more
86
+
86
87
  last_id = data.last.id
87
88
 
88
89
  params = filters.merge(starting_after: last_id).merge(params)
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "tempfile"
5
+
6
+ module Stripe
7
+ # Encodes parameters into a `multipart/form-data` payload as described by RFC
8
+ # 2388:
9
+ #
10
+ # https://tools.ietf.org/html/rfc2388
11
+ #
12
+ # This is most useful for transferring file-like objects.
13
+ #
14
+ # Parameters should be added with `#encode`. When ready, use `#body` to get
15
+ # the encoded result and `#content_type` to get the value that should be
16
+ # placed in the `Content-Type` header of a subsequent request (which includes
17
+ # a boundary value).
18
+ class MultipartEncoder
19
+ MULTIPART_FORM_DATA = "multipart/form-data"
20
+
21
+ # A shortcut for encoding a single set of parameters and finalizing a
22
+ # result.
23
+ #
24
+ # Returns an encoded body and the value that should be set in the content
25
+ # type header of a subsequent request.
26
+ def self.encode(params)
27
+ encoder = MultipartEncoder.new
28
+ encoder.encode(params)
29
+ encoder.close
30
+ [encoder.body, encoder.content_type]
31
+ end
32
+
33
+ # Gets the object's randomly generated boundary string.
34
+ attr_reader :boundary
35
+
36
+ # Initializes a new multipart encoder.
37
+ def initialize
38
+ # Kind of weird, but required by Rubocop because the unary plus operator
39
+ # is considered faster than `Stripe.new`.
40
+ @body = +""
41
+
42
+ # Chose the same number of random bytes that Go uses in its standard
43
+ # library implementation. Easily enough entropy to ensure that it won't
44
+ # be present in a file we're sending.
45
+ @boundary = SecureRandom.hex(30)
46
+
47
+ @closed = false
48
+ @first_field = true
49
+ end
50
+
51
+ # Gets the encoded body. `#close` must be called first.
52
+ def body
53
+ raise "object must be closed before getting body" unless @closed
54
+
55
+ @body
56
+ end
57
+
58
+ # Finalizes the object by writing the final boundary.
59
+ def close
60
+ raise "object already closed" if @closed
61
+
62
+ @body << "\r\n"
63
+ @body << "--#{@boundary}--"
64
+
65
+ @closed = true
66
+
67
+ nil
68
+ end
69
+
70
+ # Gets the value including boundary that should be put into a multipart
71
+ # request's `Content-Type`.
72
+ def content_type
73
+ "#{MULTIPART_FORM_DATA}; boundary=#{@boundary}"
74
+ end
75
+
76
+ # Encodes a set of parameters to the body.
77
+ #
78
+ # Note that parameters are expected to be a hash, but a "flat" hash such
79
+ # that complex substructures like hashes and arrays have already been
80
+ # appropriately Stripe-encoded. Pass a complex structure through
81
+ # `Util.flatten_params` first before handing it off to this method.
82
+ def encode(params)
83
+ raise "no more parameters can be written to closed object" if @closed
84
+
85
+ params.each do |name, val|
86
+ if val.is_a?(::File) || val.is_a?(::Tempfile)
87
+ write_field(name, val.read, filename: ::File.basename(val.path))
88
+ elsif val.respond_to?(:read)
89
+ write_field(name, val.read, filename: "blob")
90
+ else
91
+ write_field(name, val, filename: nil)
92
+ end
93
+ end
94
+
95
+ nil
96
+ end
97
+
98
+ #
99
+ # private
100
+ #
101
+
102
+ # Escapes double quotes so that the given value can be used in a
103
+ # double-quoted string and replaces any linebreak characters with spaces.
104
+ private def escape(str)
105
+ str.gsub('"', "%22").tr("\n", " ").tr("\r", " ")
106
+ end
107
+
108
+ private def write_field(name, data, filename:)
109
+ if !@first_field
110
+ @body << "\r\n"
111
+ else
112
+ @first_field = false
113
+ end
114
+
115
+ @body << "--#{@boundary}\r\n"
116
+
117
+ if filename
118
+ @body << %(Content-Disposition: form-data) +
119
+ %(; name="#{escape(name.to_s)}") +
120
+ %(; filename="#{escape(filename)}"\r\n)
121
+ @body << %(Content-Type: application/octet-stream\r\n)
122
+ else
123
+ @body << %(Content-Disposition: form-data) +
124
+ %(; name="#{escape(name.to_s)}"\r\n)
125
+ end
126
+
127
+ @body << "\r\n"
128
+ @body << data.to_s
129
+ end
130
+ end
131
+ end
@@ -41,7 +41,6 @@ module Stripe
41
41
  Invoice::OBJECT_NAME => Invoice,
42
42
  InvoiceItem::OBJECT_NAME => InvoiceItem,
43
43
  InvoiceLineItem::OBJECT_NAME => InvoiceLineItem,
44
- IssuerFraudRecord::OBJECT_NAME => IssuerFraudRecord,
45
44
  Issuing::Authorization::OBJECT_NAME => Issuing::Authorization,
46
45
  Issuing::Card::OBJECT_NAME => Issuing::Card,
47
46
  Issuing::CardDetails::OBJECT_NAME => Issuing::CardDetails,
@@ -30,7 +30,6 @@ require "stripe/resources/file_link"
30
30
  require "stripe/resources/invoice"
31
31
  require "stripe/resources/invoice_item"
32
32
  require "stripe/resources/invoice_line_item"
33
- require "stripe/resources/issuer_fraud_record"
34
33
  require "stripe/resources/issuing/authorization"
35
34
  require "stripe/resources/issuing/card"
36
35
  require "stripe/resources/issuing/card_details"
@@ -9,7 +9,7 @@ module Stripe
9
9
  include Stripe::APIOperations::Save
10
10
  extend Stripe::APIOperations::NestedResource
11
11
 
12
- OBJECT_NAME = "account".freeze
12
+ OBJECT_NAME = "account"
13
13
 
14
14
  custom_method :reject, http_verb: :post
15
15
 
@@ -35,10 +35,6 @@ module Stripe
35
35
 
36
36
  nested_resource_class_methods :login_link, operations: %i[create]
37
37
 
38
- # This method is deprecated. Please use `#external_account=` instead.
39
- save_nested_resource :bank_account
40
- deprecate :bank_account=, "#external_account=", 2017, 8
41
-
42
38
  def resource_url
43
39
  if self["id"]
44
40
  super
@@ -4,6 +4,6 @@ module Stripe
4
4
  class AccountLink < APIResource
5
5
  extend Stripe::APIOperations::Create
6
6
 
7
- OBJECT_NAME = "account_link".freeze
7
+ OBJECT_NAME = "account_link"
8
8
  end
9
9
  end
@@ -5,7 +5,7 @@ module Stripe
5
5
  include Stripe::APIOperations::Save
6
6
  include Stripe::APIOperations::Delete
7
7
 
8
- OBJECT_NAME = "alipay_account".freeze
8
+ OBJECT_NAME = "alipay_account"
9
9
 
10
10
  def resource_url
11
11
  if !respond_to?(:customer) || customer.nil?
@@ -7,7 +7,7 @@ module Stripe
7
7
  include Stripe::APIOperations::Delete
8
8
  extend Stripe::APIOperations::List
9
9
 
10
- OBJECT_NAME = "apple_pay_domain".freeze
10
+ OBJECT_NAME = "apple_pay_domain"
11
11
 
12
12
  def self.resource_url
13
13
  "/v1/apple_pay/domains"
@@ -5,20 +5,9 @@ module Stripe
5
5
  extend Stripe::APIOperations::List
6
6
  extend Stripe::APIOperations::NestedResource
7
7
 
8
- OBJECT_NAME = "application_fee".freeze
8
+ OBJECT_NAME = "application_fee"
9
9
 
10
10
  nested_resource_class_methods :refund,
11
11
  operations: %i[create retrieve update list]
12
-
13
- # If you don't need access to an updated fee object after the refund, it's
14
- # more performant to just call `fee.refunds.create` directly.
15
- def refund(params = {}, opts = {})
16
- refunds.create(params, opts)
17
-
18
- # now that a refund has been created, we expect the state of this object
19
- # to change as well (i.e. `refunded` will now be `true`) so refresh it
20
- # from the server
21
- refresh
22
- end
23
12
  end
24
13
  end
@@ -5,7 +5,7 @@ module Stripe
5
5
  include Stripe::APIOperations::Save
6
6
  extend Stripe::APIOperations::List
7
7
 
8
- OBJECT_NAME = "fee_refund".freeze
8
+ OBJECT_NAME = "fee_refund"
9
9
 
10
10
  def resource_url
11
11
  "#{ApplicationFee.resource_url}/#{CGI.escape(fee)}/refunds" \
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Stripe
4
4
  class Balance < SingletonAPIResource
5
- OBJECT_NAME = "balance".freeze
5
+ OBJECT_NAME = "balance"
6
6
  end
7
7
  end
@@ -4,6 +4,6 @@ module Stripe
4
4
  class BalanceTransaction < APIResource
5
5
  extend Stripe::APIOperations::List
6
6
 
7
- OBJECT_NAME = "balance_transaction".freeze
7
+ OBJECT_NAME = "balance_transaction"
8
8
  end
9
9
  end
@@ -6,7 +6,7 @@ module Stripe
6
6
  extend Stripe::APIOperations::List
7
7
  include Stripe::APIOperations::Save
8
8
 
9
- OBJECT_NAME = "bank_account".freeze
9
+ OBJECT_NAME = "bank_account"
10
10
 
11
11
  def verify(params = {}, opts = {})
12
12
  resp, opts = request(:post, resource_url + "/verify", params, opts)
@@ -6,7 +6,7 @@ module Stripe
6
6
  class BitcoinReceiver < APIResource
7
7
  extend Stripe::APIOperations::List
8
8
 
9
- OBJECT_NAME = "bitcoin_receiver".freeze
9
+ OBJECT_NAME = "bitcoin_receiver"
10
10
 
11
11
  def self.resource_url
12
12
  "/v1/bitcoin/receivers"
@@ -6,7 +6,7 @@ module Stripe
6
6
  # Sources API instead: https://stripe.com/docs/sources/bitcoin
7
7
  extend Stripe::APIOperations::List
8
8
 
9
- OBJECT_NAME = "bitcoin_transaction".freeze
9
+ OBJECT_NAME = "bitcoin_transaction"
10
10
 
11
11
  def self.resource_url
12
12
  "/v1/bitcoin/transactions"
@@ -5,7 +5,7 @@ module Stripe
5
5
  extend Stripe::APIOperations::List
6
6
  include Stripe::APIOperations::Save
7
7
 
8
- OBJECT_NAME = "capability".freeze
8
+ OBJECT_NAME = "capability"
9
9
 
10
10
  def resource_url
11
11
  if !respond_to?(:account) || account.nil?
@@ -6,7 +6,7 @@ module Stripe
6
6
  extend Stripe::APIOperations::List
7
7
  include Stripe::APIOperations::Save
8
8
 
9
- OBJECT_NAME = "card".freeze
9
+ OBJECT_NAME = "card"
10
10
 
11
11
  def resource_url
12
12
  if respond_to?(:recipient) && !recipient.nil? && !recipient.empty?