lightrail_client 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9ac91b33c8fcc53e6a2459a44a826c604ed036c6
4
+ data.tar.gz: a825aed32553201cfdde45ccc1900b89b25c3396
5
+ SHA512:
6
+ metadata.gz: edc6f2c48d63a7db114f132666c7efbac10061047134f2db934c2958ce2788dd1ec4901b9c8ea6518be8965b624567f49b324480542b5fcb0edaa2f144701c31
7
+ data.tar.gz: 0d37e39da7491d93039b362c4d4fa64044056f5eb4a11cdd40181fe969f596ce9307fcd54ab60849d53f05c2ed3fb6f2ac076b3d0a38782bf5fd97febc053ec8
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ # rspec failure tracking
12
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.1
5
+ before_install: gem install bundler -v 1.15.4
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in lightrail_client.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Tana Jukes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,300 @@
1
+ # Lightrail Client Gem (beta)
2
+
3
+ Lightrail is a modern platform for digital account credits, gift cards, promotions, and points (to learn more, visit [Lightrail](https://www.lightrail.com/)). The Lightrail Client Gem is a basic library for developers to easily connect with the Lightrail API using Ruby. If you are looking for specific use cases or other languages, check out the complete list of all [Lightrail libraries and integrations](https://github.com/Giftbit/Lightrail-API-Docs/blob/master/docs/client-libraries.md).
4
+
5
+ ## Features
6
+
7
+ The following features are supported in this version:
8
+
9
+ - Account Credits: create, retrieve, charge, refund, balance-check, and fund.
10
+ - Gift Cards: charge, refund, balance-check, and fund.
11
+
12
+ Note that the Lightrail API supports many other features and we are working on covering them in this gem. For a complete list of Lightrail API features check out the [Lightrail API documentation](https://www.lightrail.com/docs/).
13
+
14
+ ## Related Projects
15
+
16
+ Check out the full list of [Lightrail client libraries and integrations](https://github.com/Giftbit/Lightrail-API-Docs/blob/master/docs/client-libraries.md).
17
+
18
+ ## Usage
19
+
20
+ Before using any parts of the library, you'll need to configure it to use your API key:
21
+
22
+ ```ruby
23
+ Lightrail.api_key = "<your lightrail API key>"
24
+ ```
25
+
26
+ *A note on sample code snippets: for reasons of legibility, the output for most calls has been simplified. Attributes of response objects that are not relevant here have been omitted.*
27
+
28
+ ### Use Case: Account Credits Powered by Lightrail
29
+
30
+ For a quick demonstration of implementing account credits using this library, see our [Accounts Quickstart](https://github.com/Giftbit/Lightrail-API-Docs/blob/master/docs/quickstart/accounts.md).
31
+
32
+
33
+ ### Use Case: Gift Cards
34
+
35
+ **Looking for Lightrail's Drop-In Gift Card Solution?**
36
+
37
+ Check out our [Drop-in Gift Card documentation](https://github.com/Giftbit/Lightrail-API-Docs/blob/master/docs/quickstart/drop-in-gift-cards.md#drop-in-gift-cards) to get started.
38
+
39
+ **Prefer to build it yourself?**
40
+
41
+ The remainder of this document is a detailed overview of the methods this library offers for managing Gift Cards. It assumes familiarity with the concepts in our [Gift Card guide](https://github.com/Giftbit/Lightrail-API-Docs/blob/master/use-cases/gift-card.md).
42
+
43
+ #### Balance Check
44
+
45
+ There are several ways to check the balance of a gift card or code. Because you can attach conditional value to a card/code (for example, "get $5 off when you buy a red hat"), the available balance can vary depending on the transaction context.
46
+
47
+ ##### Maximum Value
48
+
49
+ To get the maximum value of a card/code, i.e. the sum of all active value stores, call either `Card.get_maximum_value(<CARD ID>)` or `Code.get_maximum_value(<CODE>)`. This method will return an integer which represents the sum of all active value stores in the smallest currency unit (e.g. cents):
50
+
51
+ ```ruby
52
+ maximum_gift_value = Lightrail::Card.get_maximum_value("<GIFT CARD ID>")
53
+ # or use the code:
54
+ # maximum_gift_value = Lightrail::Code.get_maximum_value("<GIFT CODE>")
55
+
56
+ #=> 3500
57
+ ```
58
+
59
+ ##### Card/Code Details
60
+
61
+ If you would like to see a breakdown of how the value is stored on a card or code you can use the `.get_details` method. This will return a breakdown of all attached value stores, along with other important information:
62
+
63
+ ```
64
+ gift_details = Lightrail::Card.get_details("<GIFT CARD ID>")
65
+ # or use the code:
66
+ # gift_details = Lightrail::Code.get_details("<GIFT CODE>")
67
+
68
+ #=> {
69
+ "valueStores": [
70
+ {
71
+ "valueStoreType": "PRINCIPAL",
72
+ "value": 483,
73
+ "state": "ACTIVE",
74
+ "expires": null,
75
+ "startDate": null,
76
+ "programId": "program-123456",
77
+ "valueStoreId": "value-11111111",
78
+ "restrictions": []
79
+ },
80
+ {
81
+ "valueStoreType": "ATTACHED",
82
+ "value": 1234,
83
+ "state": "ACTIVE",
84
+ "expires": "2017-11-13T19:29:31.613Z",
85
+ "startDate": null,
86
+ "programId": "program-7890",
87
+ "valueStoreId": "value-2222222",
88
+ "restrictions": ["Valid for purchase of a red hat"]
89
+ },
90
+ {
91
+ "valueStoreType": "ATTACHED",
92
+ "value": 500,
93
+ "state": "EXPIRED",
94
+ "expires": "2017-09-13T19:29:37.464Z",
95
+ "startDate": null,
96
+ "programId": "program-24680",
97
+ "valueStoreId": "value-3333333",
98
+ "restrictions": ["Cart must have five or more items"]
99
+ }
100
+ ],
101
+ "currency": "USD",
102
+ "cardType": "GIFT_CARD",
103
+ "asAtDate": "2017-11-06T19:29:41.533Z",
104
+ "cardId": "card-12q4wresdgf6ey",
105
+ "codeLastFour": "WXYZ"
106
+ }
107
+ }
108
+ ```
109
+
110
+ These details can be useful for showing a customer a summary of their gift balance, or for incentivizing further spending (e.g. "Add a red hat to your order to get $12.34 off").
111
+
112
+ ##### Simulate Transaction
113
+
114
+ If you would like to know how much is available for a specific transaction, use the `.simulate_charge` method. Simply pass in all the same parameters as you would to make a regular charge (see below) **including metadata** so that the Lightrail engine can assess whether necessary conditions are met for any attached value.
115
+
116
+ The `value` of the response will indicate the maximum amount that can be charged given the context of the transaction, which can be useful when presenting your customer with a confirmation dialogue. The `value` is a drawdown amount and will therefore be negative:
117
+
118
+ ```ruby
119
+ simulated_charge = Lightrail::Card.simulate_charge({
120
+ value: -1850,
121
+ currency: 'USD',
122
+ card_id: '<GIFT CARD ID>',
123
+ metadata: {cart: {items_total: 5}},
124
+ })
125
+ #=> {
126
+ "value"=>-1550,
127
+ "userSuppliedId"=>"2bfb5ccb",
128
+ "transactionType"=>"DRAWDOWN",
129
+ "currency"=>"USD",
130
+ "transactionBreakdown": [
131
+ {
132
+ "value": -1234,
133
+ "valueAvailableAfterTransaction": 0,
134
+ "valueStoreId": "value-4f9a362e7206445796d934727e0d2b27"
135
+ },
136
+ {
137
+ "value": -616,
138
+ "valueAvailableAfterTransaction": 0,
139
+ "valueStoreId": "value-9850b36634b541f5bc6fd280b0198b3d",
140
+ "restrictions": ["Cart must have five or more items"],
141
+ }
142
+ ],
143
+ "transactionId": null,
144
+ "dateCreated": null,
145
+ #...
146
+ }
147
+ ```
148
+
149
+ Note that because this is a simulated transaction and not a real transaction, the `transactionId` and `dateCreated` will both be `null`.
150
+
151
+ #### Charging a Gift Card
152
+
153
+ In order to make a charge, you can call `.charge` on either a `Code` or a `Card`. The minimum required parameters are the `fullCode` or `cardId`, the `currency`, and the `value` of the transaction (a negative integer in the smallest currency unit, e.g., 500 cents is 5 USD):
154
+
155
+ ```ruby
156
+ gift_charge = Lightrail::Code.charge({
157
+ value: -2500,
158
+ currency: 'USD',
159
+ code: '<GIFT CODE>'
160
+ })
161
+ #=> {
162
+ "value"=>-1850,
163
+ "userSuppliedId"=>"2bfb5ccb",
164
+ "transactionType"=>"DRAWDOWN",
165
+ "currency"=>"USD",
166
+ #...
167
+ }
168
+ ```
169
+
170
+ **A note on idempotency:** All calls to create or act on transactions (refund, void, capture) can optionally take a `userSuppliedId` parameter. The `userSuppliedId` is a client-side identifier (unique string) which is used to ensure idempotency (for more details, see the [API documentation](https://www.lightrail.com/docs/)). If you do not provide a `userSuppliedId`, the gem will create one for you for any calls that require one.
171
+
172
+ ```ruby
173
+ gift_charge = Lightrail::Code.charge({
174
+ value: -1850,
175
+ currency: 'USD',
176
+ code: '<GIFT CODE>',
177
+ userSuppliedId: 'order-13jg9s0era9023-u9a-0ea'
178
+ })
179
+ ```
180
+
181
+ Note that Lightrail does not support currency exchange and the currency provided to these methods must match the currency of the gift card.
182
+
183
+ For more details on the parameters that you can pass in for a charge request and the response that you will get back, see the [API documentation](https://www.lightrail.com/docs/).
184
+
185
+ #### Authorize-Capture Flow
186
+
187
+ By adding ` pending: true` to your charge param hash when calling either `Card.charge` or `Code.charge`, you can create a pre-authorized pending transaction. When you are ready to capture or void it, you will call `Transaction.capture` or `Transaction.void` and pass in the response you get back from the call to create the pending charge:
188
+
189
+ ```ruby
190
+ gift_charge = Lightrail::Code.charge({
191
+ value: -1850,
192
+ currency: 'USD',
193
+ code: '<GIFT CODE>',
194
+ pending: true,
195
+ })
196
+ # later on
197
+ Lightrail::Transaction.capture(gift_charge)
198
+ #=> {
199
+ "value"=>-1850,
200
+ "userSuppliedId"=>"12c2d18f",
201
+ "dateCreated"=>"2017-05-29T13:37:02.756Z",
202
+ "transactionType"=>"DRAWDOWN",
203
+ "transactionAccessMethod"=>"RAWCODE",
204
+ "cardId"=>"<GIFT CARD ID>",
205
+ "currency"=>"USD",
206
+ "transactionId"=>"transaction-8483d9",
207
+ "parentTransactionId"=>"transaction-cf353236"
208
+ }
209
+
210
+ # or
211
+ Lightrail::Transaction.void(gift_charge)
212
+ #=> {
213
+ "value"=>-1850,
214
+ "userSuppliedId"=>"12c2d18f",
215
+ "dateCreated"=>"2017-05-29T13:37:02.756Z",
216
+ "transactionType"=>"PENDING_VOID",
217
+ "transactionAccessMethod"=>"RAWCODE",
218
+ "cardId"=>"<GIFT CARD ID>",
219
+ "currency"=>"USD",
220
+ "transactionId"=>"transaction-d10e76",
221
+ "parentTransactionId"=>"transaction-cf353236"
222
+ }
223
+ ```
224
+
225
+ Note that `Transaction.void` and `Transaction.capture` will each return a **new transaction** and will not modify the original pending transaction they are called on. These new transactions will have their own `transactionId`. If you need to record the transaction ID of the captured or canceled charge, you can get it from the hash returned by these methods.
226
+
227
+ #### Refunding a Charge
228
+
229
+ You can undo a charge by calling `Transaction.refund` and passing in the details of the transaction you wish to refund. This will create a new `refund` transaction which will return the charged amount back to the card. If you need the transaction ID of the refund transaction, you can find it in the response from the API.
230
+
231
+ ```ruby
232
+ gift_charge = Lightrail::Code.charge(<CHARGE PARAMS>)
233
+
234
+ # later on
235
+ Lightrail::Transaction.refund(gift_charge)
236
+ #=> {
237
+ "value"=>1850,
238
+ "userSuppliedId"=>"873b08ab",
239
+ "dateCreated"=>"2017-05-29T13:37:02.756Z",
240
+ "transactionType"=>"DRAWDOWN_REFUND",
241
+ "transactionAccessMethod"=>"CARDID",
242
+ "cardId"=>"<GIFT CARD ID>",
243
+ "currency"=>"USD",
244
+ "transactionId"=>"transaction-0f2a67",
245
+ "parentTransactionId"=>"transaction-2271e3"
246
+ }
247
+ ```
248
+
249
+ Note that this does not necessarily mean that the refunded amount is available for a re-charge. In the edge case where the funds for the original charge came from a promotion which has now expired, refunding will return those funds back to the now-expired value store and therefore the value will not be available for re-charge. To learn more about using value stores for temporary promotions, see the [Lightrail API docs](https://github.com/Giftbit/Lightrail-API-Docs/blob/master/use-cases/promotions.md).
250
+
251
+ #### Funding a Gift Card
252
+
253
+ To fund a gift card, you can call `Card.fund`. Note that the Lightrail API does not permit funding a gift card by its `code` and the only way to fund a card is by providing its `cardId`:
254
+
255
+ ```ruby
256
+ gift_fund = Lightrail::Card.fund({
257
+ value: 500,
258
+ currency: 'USD',
259
+ card_id: '<GIFT CARD ID>',
260
+ })
261
+ #=> {
262
+ "value"=>500,
263
+ "userSuppliedId"=>"7676c986",
264
+ "dateCreated"=>"2017-05-29T13:37:02.756Z",
265
+ "transactionType"=>"FUND",
266
+ "transactionAccessMethod"=>"CARDID",
267
+ "cardId"=>"<GIFT CARD ID>",
268
+ "currency"=>"USD",
269
+ "transactionId"=>"transaction-dee3ee7"
270
+ }
271
+ ```
272
+
273
+
274
+ ## Installation
275
+
276
+ This gem is in alpha mode and is not yet available on RubyGems. You can use it in your project by adding this line to your application's Gemfile:
277
+
278
+ ```ruby
279
+ gem 'lightrail_client', :git => 'https://github.com/Giftbit/lightrail-client-ruby.git'
280
+ ```
281
+
282
+ And then execute:
283
+
284
+ ```
285
+ $ bundle
286
+ ```
287
+
288
+ ## Contributing
289
+
290
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Giftbit/lightrail-client-ruby.
291
+
292
+ ## Development
293
+
294
+ After checking out the repo, run `bin/setup` to install dependencies, then run `bundle exec rspec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
295
+
296
+ To install this gem onto your local machine, run `bundle exec rake install`.
297
+
298
+ ## License
299
+
300
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "lightrail_client"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,29 @@
1
+ require "faraday"
2
+ require "openssl"
3
+ require "json"
4
+ require "securerandom"
5
+ require "jwt"
6
+ require "base64"
7
+
8
+ require "lightrail_client/version"
9
+
10
+ require "lightrail_client/constants"
11
+ require "lightrail_client/errors"
12
+ require "lightrail_client/validator"
13
+ require "lightrail_client/connection"
14
+ require "lightrail_client/shopper_token_factory"
15
+
16
+ require "lightrail_client/lightrail_object"
17
+ require "lightrail_client/ping"
18
+ require "lightrail_client/transaction"
19
+ require "lightrail_client/card"
20
+ require "lightrail_client/code"
21
+ require "lightrail_client/contact"
22
+ require "lightrail_client/account"
23
+
24
+ module Lightrail
25
+ class << self
26
+ attr_accessor :api_base, :api_key, :shared_secret
27
+ end
28
+ @api_base = 'https://api.lightrail.com/v1'
29
+ end
@@ -0,0 +1,93 @@
1
+ module Lightrail
2
+ class Account < Lightrail::LightrailObject
3
+ def self.create(account_params)
4
+ validated_params = Lightrail::Validator.set_params_for_account_create!(account_params)
5
+
6
+ # Make sure contact exists first
7
+ contact_id = Lightrail::Validator.get_contact_id(account_params)
8
+ shopper_id = Lightrail::Validator.get_shopper_id(account_params)
9
+
10
+ if contact_id
11
+ contact = Lightrail::Contact.retrieve_by_contact_id(contact_id)
12
+ if shopper_id && (contact['userSuppliedId'] != shopper_id)
13
+ raise Lightrail::LightrailArgumentError.new("Account creation error: you've specified both a contactId and a shopperId for this account, but the contact with that contactId has a different shopperId.")
14
+ end
15
+
16
+ elsif shopper_id
17
+ contact = Lightrail::Contact.retrieve_or_create_by_shopper_id(shopper_id)
18
+ end
19
+
20
+ # If the contact already has an account in that currency, return it
21
+ account_card = Lightrail::Account.retrieve({contact_id: contact['contactId'], currency: account_params[:currency]})
22
+ return account_card['cardId'] if account_card
23
+
24
+ params_with_contact_id = validated_params.clone
25
+ params_with_contact_id[:contactId] = contact['contactId']
26
+ response = Lightrail::Connection.send :make_post_request_and_parse_response, "cards", params_with_contact_id
27
+ response['card']
28
+ end
29
+
30
+ def self.retrieve(account_retrieval_params)
31
+ new_params = account_retrieval_params.clone
32
+ currency = new_params[:currency] || new_params['currency']
33
+ Lightrail::Validator.validate_currency!(currency)
34
+ Lightrail::Validator.set_contactId_from_contact_or_shopper_id!(new_params, new_params)
35
+ contact_id = new_params[:contactId]
36
+ response = Lightrail::Connection.send :make_get_request_and_parse_response, "cards?cardType=ACCOUNT_CARD&contactId=#{CGI::escape(contact_id)}&currency=#{CGI::escape(currency)}"
37
+ response['cards'][0]
38
+ end
39
+
40
+ def self.charge(charge_params)
41
+ params_with_account_card_id = self.replace_contact_id_or_shopper_id_with_card_id(charge_params)
42
+ Lightrail::Card.charge(params_with_account_card_id)
43
+ end
44
+
45
+ def self.simulate_charge(charge_params)
46
+ params_with_account_card_id = self.replace_contact_id_or_shopper_id_with_card_id(charge_params)
47
+ Lightrail::Card.simulate_charge(params_with_account_card_id)
48
+ end
49
+
50
+ def self.fund(fund_params)
51
+ params_with_account_card_id = self.replace_contact_id_or_shopper_id_with_card_id(fund_params)
52
+ Lightrail::Card.fund(params_with_account_card_id)
53
+ end
54
+
55
+ def self.get_account_details(account_details_params)
56
+ params_with_account_card_id = self.replace_contact_id_or_shopper_id_with_card_id(account_details_params)
57
+ Lightrail::Card.get_details(params_with_account_card_id[:card_id])
58
+ end
59
+
60
+ def self.get_maximum_account_value(max_account_value_params)
61
+ params_with_account_card_id = self.replace_contact_id_or_shopper_id_with_card_id(max_account_value_params)
62
+ Lightrail::Card.get_maximum_value(params_with_account_card_id[:card_id])
63
+ end
64
+
65
+
66
+ private
67
+
68
+ def self.replace_contact_id_or_shopper_id_with_card_id(transaction_params)
69
+ contact_id = Lightrail::Contact.get_contact_id_from_id_or_shopper_id(transaction_params)
70
+
71
+ if contact_id
72
+ account_card_id = self.retrieve({contact_id: contact_id, currency: transaction_params[:currency]})['cardId']
73
+ elsif !Lightrail::Validator.has_valid_card_id?(transaction_params)
74
+ raise Lightrail::LightrailArgumentError.new("Method replace_contact_id_or_shopper_id_with_card_id could not find contact - no contact_id or shopper_id in transaction_params: #{transaction_params.inspect}")
75
+ end
76
+
77
+ params_with_card_id = transaction_params.clone
78
+ params_with_card_id[:card_id] = account_card_id if account_card_id
79
+ params_with_card_id.delete(:contact_id)
80
+ params_with_card_id.delete(:shopper_id)
81
+ params_with_card_id
82
+ end
83
+
84
+ def self.set_account_card_type(create_account_params)
85
+ if (create_account_params['cardType'] && create_account_params['cardType'] != 'ACCOUNT_CARD') ||
86
+ (create_account_params[:cardType] && create_account_params[:cardType] != 'ACCOUNT_CARD')
87
+ raise Lightrail::LightrailArgumentError.new("Cannot create account card if cardType set to value other than 'ACCOUNT_CARD': #{create_account_params.inspect}")
88
+ end
89
+ create_account_params[:cardType] = 'ACCOUNT_CARD'
90
+ end
91
+
92
+ end
93
+ end