stripe-ruby-mock 2.5.4 → 2.5.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -1
  3. data/README.md +1 -1
  4. data/lib/stripe_mock.rb +4 -0
  5. data/lib/stripe_mock/api/webhooks.rb +2 -0
  6. data/lib/stripe_mock/data.rb +78 -17
  7. data/lib/stripe_mock/data/list.rb +7 -2
  8. data/lib/stripe_mock/instance.rb +37 -3
  9. data/lib/stripe_mock/request_handlers/accounts.rb +16 -0
  10. data/lib/stripe_mock/request_handlers/charges.rb +7 -8
  11. data/lib/stripe_mock/request_handlers/customers.rb +2 -2
  12. data/lib/stripe_mock/request_handlers/ephemeral_key.rb +13 -0
  13. data/lib/stripe_mock/request_handlers/helpers/card_helpers.rb +1 -0
  14. data/lib/stripe_mock/request_handlers/helpers/coupon_helpers.rb +10 -11
  15. data/lib/stripe_mock/request_handlers/helpers/subscription_helpers.rb +20 -2
  16. data/lib/stripe_mock/request_handlers/invoices.rb +1 -1
  17. data/lib/stripe_mock/request_handlers/products.rb +43 -0
  18. data/lib/stripe_mock/request_handlers/refunds.rb +6 -3
  19. data/lib/stripe_mock/request_handlers/subscription_items.rb +36 -0
  20. data/lib/stripe_mock/request_handlers/subscriptions.rb +40 -22
  21. data/lib/stripe_mock/request_handlers/tax_rates.rb +36 -0
  22. data/lib/stripe_mock/request_handlers/tokens.rb +2 -2
  23. data/lib/stripe_mock/request_handlers/transfers.rb +10 -4
  24. data/lib/stripe_mock/request_handlers/validators/param_validators.rb +3 -0
  25. data/lib/stripe_mock/test_strategies/base.rb +4 -2
  26. data/lib/stripe_mock/version.rb +1 -1
  27. data/lib/stripe_mock/webhook_fixtures/charge.dispute.funds_reinstated.json +88 -0
  28. data/lib/stripe_mock/webhook_fixtures/charge.dispute.funds_withdrawn.json +88 -0
  29. data/lib/stripe_mock/webhook_fixtures/customer.subscription.created.json +2 -2
  30. data/lib/stripe_mock/webhook_fixtures/customer.subscription.deleted.json +2 -2
  31. data/lib/stripe_mock/webhook_fixtures/customer.subscription.trial_will_end.json +2 -2
  32. data/lib/stripe_mock/webhook_fixtures/customer.subscription.updated.json +3 -3
  33. data/lib/stripe_mock/webhook_fixtures/invoice.created.json +3 -2
  34. data/lib/stripe_mock/webhook_fixtures/invoice.payment_failed.json +1 -1
  35. data/lib/stripe_mock/webhook_fixtures/invoice.payment_succeeded.json +1 -1
  36. data/lib/stripe_mock/webhook_fixtures/invoice.updated.json +3 -2
  37. data/lib/stripe_mock/webhook_fixtures/plan.created.json +1 -1
  38. data/lib/stripe_mock/webhook_fixtures/plan.deleted.json +1 -1
  39. data/lib/stripe_mock/webhook_fixtures/plan.updated.json +1 -1
  40. data/spec/instance_spec.rb +31 -0
  41. data/spec/shared_stripe_examples/account_examples.rb +27 -0
  42. data/spec/shared_stripe_examples/charge_examples.rb +23 -14
  43. data/spec/shared_stripe_examples/customer_examples.rb +11 -1
  44. data/spec/shared_stripe_examples/ephemeral_key_examples.rb +17 -0
  45. data/spec/shared_stripe_examples/invoice_examples.rb +5 -5
  46. data/spec/shared_stripe_examples/plan_examples.rb +19 -4
  47. data/spec/shared_stripe_examples/product_example.rb +65 -0
  48. data/spec/shared_stripe_examples/refund_examples.rb +16 -10
  49. data/spec/shared_stripe_examples/subscription_examples.rb +176 -18
  50. data/spec/shared_stripe_examples/subscription_items_examples.rb +75 -0
  51. data/spec/shared_stripe_examples/tax_rate_examples.rb +42 -0
  52. data/spec/shared_stripe_examples/transfer_examples.rb +61 -30
  53. data/spec/support/stripe_examples.rb +4 -1
  54. metadata +16 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 771c4e4adbc92b37176d94b8f91fb1f2bef740ba
4
- data.tar.gz: 86c21735586588ea3aabc958d3eb93dda6dbc7ec
3
+ metadata.gz: a15c09639e0a93df5b15cc5a45646c0fb189460e
4
+ data.tar.gz: b8cdf0692aba8099229f66e1906e691dd40075aa
5
5
  SHA512:
6
- metadata.gz: 0c747fed9cf7824d59fdb1be3ec1b399beabd69d54d08101c1c46d96792fe400e4d7b8c1aa6a816c981d58d902a3fdf46c6edff21bd280a07615799384a4a82f
7
- data.tar.gz: 68b2ab17e0f504f783bd8c5a20893ec89bee491a29cac16a824d8cdb45e48f7a2a34e307b153feb7fe58662ee661d2732c4d838391cf23497ea20fe0a90393a5
6
+ metadata.gz: 9d70a5de8ae26c090632d00c1013d5ca63933fee41e40561b2e713b511a1c1347b21485b1d1c4eb8ee6837b6f4ec9a9f8e857a7bfa0785ac2182b8190180bfbb
7
+ data.tar.gz: 542f7a6f6c8743269e0337c1fc0b4fabef97003fe4d945c077ab4e16fe908bf7c21996dcb8c25ade7a4975a273a51972621d8d997e35a86f00dcb5287ae63d89
data/.travis.yml CHANGED
@@ -17,7 +17,7 @@ script: "bundle exec rspec && bundle exec rspec -t live"
17
17
 
18
18
  env:
19
19
  global:
20
- - IS_TRAVIS=true STRIPE_TEST_SECRET_KEY_A=sk_test_sXdhUWu3NhrB7r1tkK0zZfMW STRIPE_TEST_SECRET_KEY_B=sk_test_uPfIX9ziFNloXwtSdDPJTdnh STRIPE_TEST_SECRET_KEY_C=sk_test_waSy1TaP2RNEpoz9t2pFCysm STRIPE_TEST_SECRET_KEY_D=sk_test_Z1mQZNehRFmI3EN9mHnMafnq
20
+ - IS_TRAVIS=true STRIPE_TEST_SECRET_KEY_A=sk_test_Ut2MSlZANdT3iDALdGhyLymy STRIPE_TEST_SECRET_KEY_B=sk_test_JXtzss9tHOG1ofIyEZgoUP4Q STRIPE_TEST_SECRET_KEY_C=sk_test_ZR5nVz9p3ivsqVa7mYB0sFep STRIPE_TEST_SECRET_KEY_D=sk_test_ZR5nVz9p3ivsqVa7mYB0sFep
21
21
 
22
22
  notifications:
23
23
  webhooks:
data/README.md CHANGED
@@ -12,7 +12,7 @@ This gem has unexpectedly grown in popularity and I've gotten pretty busy, so I'
12
12
 
13
13
  In your gemfile:
14
14
 
15
- gem 'stripe-ruby-mock', '~> 2.5.4', :require => 'stripe_mock'
15
+ gem 'stripe-ruby-mock', '~> 2.5.8', :require => 'stripe_mock'
16
16
 
17
17
  ## Features
18
18
 
data/lib/stripe_mock.rb CHANGED
@@ -67,8 +67,12 @@ require 'stripe_mock/request_handlers/refunds.rb'
67
67
  require 'stripe_mock/request_handlers/transfers.rb'
68
68
  require 'stripe_mock/request_handlers/payouts.rb'
69
69
  require 'stripe_mock/request_handlers/subscriptions.rb'
70
+ require 'stripe_mock/request_handlers/subscription_items.rb'
70
71
  require 'stripe_mock/request_handlers/tokens.rb'
71
72
  require 'stripe_mock/request_handlers/country_spec.rb'
73
+ require 'stripe_mock/request_handlers/ephemeral_key.rb'
74
+ require 'stripe_mock/request_handlers/products.rb'
75
+ require 'stripe_mock/request_handlers/tax_rates.rb'
72
76
  require 'stripe_mock/instance'
73
77
 
74
78
  require 'stripe_mock/test_strategies/base.rb'
@@ -50,6 +50,8 @@ module StripeMock
50
50
  'charge.dispute.created',
51
51
  'charge.dispute.updated',
52
52
  'charge.dispute.closed',
53
+ 'charge.dispute.funds_reinstated',
54
+ 'charge.dispute.funds_withdrawn',
53
55
  'customer.source.created',
54
56
  'customer.source.deleted',
55
57
  'customer.source.updated',
@@ -101,6 +101,22 @@ module StripeMock
101
101
  }.merge(params)
102
102
  end
103
103
 
104
+ def self.mock_tax_rate(params)
105
+ {
106
+ id: 'test_cus_default',
107
+ object: 'tax_rate',
108
+ active: true,
109
+ created: 1559079603,
110
+ description: nil,
111
+ display_name: 'VAT',
112
+ inclusive: false,
113
+ jurisdiction: 'EU',
114
+ livemode: false,
115
+ metadata: {},
116
+ percentage: 21.0
117
+ }.merge(params)
118
+ end
119
+
104
120
  def self.mock_customer(sources, params)
105
121
  cus_id = params[:id] || "test_cus_default"
106
122
  currency = params[:currency] || StripeMock.default_currency
@@ -111,6 +127,7 @@ module StripeMock
111
127
  object: "customer",
112
128
  created: 1372126710,
113
129
  id: cus_id,
130
+ name: nil,
114
131
  livemode: false,
115
132
  delinquent: false,
116
133
  discount: nil,
@@ -243,7 +260,8 @@ module StripeMock
243
260
  cvc_check: nil,
244
261
  address_line1_check: nil,
245
262
  address_zip_check: nil,
246
- tokenization_method: nil
263
+ tokenization_method: nil,
264
+ metadata: {}
247
265
  }, params)
248
266
  end
249
267
 
@@ -261,7 +279,8 @@ module StripeMock
261
279
  status: 'new',
262
280
  account_holder_name: 'John Doe',
263
281
  account_holder_type: 'individual',
264
- fingerprint: "aBcFinGerPrINt123"
282
+ fingerprint: "aBcFinGerPrINt123",
283
+ metadata: {}
265
284
  }.merge(params)
266
285
  end
267
286
 
@@ -289,6 +308,7 @@ module StripeMock
289
308
  current_period_start: 1308595038,
290
309
  current_period_end: 1308681468,
291
310
  status: 'trialing',
311
+ trial_from_plan: false,
292
312
  plan: {
293
313
  interval: 'month',
294
314
  amount: 7500,
@@ -332,7 +352,10 @@ module StripeMock
332
352
  lines << Data.mock_line_item() if lines.empty?
333
353
  invoice = {
334
354
  id: 'in_test_invoice',
335
- date: 1349738950,
355
+ status: 'open',
356
+ invoice_pdf: 'pdf_url',
357
+ hosted_invoice_url: 'hosted_invoice_url',
358
+ created: 1349738950,
336
359
  period_end: 1349738950,
337
360
  period_start: 1349738950,
338
361
  lines: {
@@ -353,12 +376,13 @@ module StripeMock
353
376
  paid: false,
354
377
  receipt_number: nil,
355
378
  statement_descriptor: nil,
356
- tax: nil,
379
+ tax: 10,
357
380
  tax_percent: nil,
358
381
  webhooks_delivered_at: 1349825350,
359
382
  livemode: false,
360
383
  attempt_count: 0,
361
- amount_due: nil,
384
+ amount_due: 100,
385
+ amount_paid: 0,
362
386
  currency: currency,
363
387
  starting_balance: 0,
364
388
  ending_balance: nil,
@@ -393,6 +417,11 @@ module StripeMock
393
417
  start: 1349738920,
394
418
  end: 1349738920
395
419
  },
420
+ tax_amounts: [
421
+ {
422
+ amount: 10
423
+ }
424
+ ],
396
425
  quantity: nil,
397
426
  subscription: nil,
398
427
  plan: nil,
@@ -406,7 +435,7 @@ module StripeMock
406
435
  {
407
436
  id: "test_ii",
408
437
  object: "invoiceitem",
409
- date: 1349738920,
438
+ created: 1349738920,
410
439
  amount: 1099,
411
440
  livemode: false,
412
441
  proration: false,
@@ -507,6 +536,21 @@ module StripeMock
507
536
  }.merge(params)
508
537
  end
509
538
 
539
+ def self.mock_product(params = {})
540
+ {
541
+ id: "default_test_prod",
542
+ object: "product",
543
+ active: true,
544
+ created: 1556896214,
545
+ livemode: false,
546
+ metadata: {},
547
+ name: "Default Test Product",
548
+ statement_descriptor: "PRODUCT",
549
+ type: "service",
550
+ updated: 1556918200,
551
+ }.merge(params)
552
+ end
553
+
510
554
  def self.mock_recipient(cards, params={})
511
555
  rp_id = params[:id] || "test_rp_default"
512
556
  cards.each {|card| card[:recipient] = rp_id}
@@ -594,31 +638,29 @@ module StripeMock
594
638
  currency = params[:currency] || StripeMock.default_currency
595
639
  id = params[:id] || 'tr_test_transfer'
596
640
  {
597
- :status => 'pending',
598
641
  :amount => 100,
599
- :account => {
600
- :object => 'bank_account',
601
- :country => 'US',
602
- :bank_name => 'STRIPE TEST BANK',
603
- :last4 => '6789'
604
- },
605
- :recipient => 'test_recipient',
606
- :fee => 0,
607
- :fee_details => [],
642
+ :amount_reversed => 0,
643
+ :balance_transaction => "txn_2dyYXXP90MN26R",
608
644
  :id => id,
609
645
  :livemode => false,
610
646
  :metadata => {},
611
647
  :currency => currency,
612
648
  :object => "transfer",
613
- :date => 1304114826,
649
+ :created => 1304114826,
614
650
  :description => "Transfer description",
615
651
  :reversed => false,
616
652
  :reversals => {
617
653
  :object => "list",
654
+ :data => [],
618
655
  :total_count => 0,
619
656
  :has_more => false,
620
657
  :url => "/v1/transfers/#{id}/reversals"
621
658
  },
659
+ :destination => "acct_164wxjKbnvuxQXGu",
660
+ :destination_payment => "py_164xRvKbnvuxQXGuVFV2pZo1",
661
+ :source_transaction => "ch_164xRv2eZvKYlo2Clu1sIJWB",
662
+ :source_type => "card",
663
+ :transfer_group => "group_ch_164xRv2eZvKYlo2Clu1sIJWB",
622
664
  }.merge(params)
623
665
  end
624
666
 
@@ -1027,5 +1069,24 @@ module StripeMock
1027
1069
  quantity: 2
1028
1070
  }.merge(params)
1029
1071
  end
1072
+
1073
+ def self.mock_ephemeral_key(**params)
1074
+ created = Time.now.to_i
1075
+ expires = created + 34_000
1076
+ {
1077
+ id: "ephkey_default",
1078
+ object: "ephemeral_key",
1079
+ associated_objects: [
1080
+ {
1081
+ id: params[:customer],
1082
+ type: "customer"
1083
+ }
1084
+ ],
1085
+ created: created,
1086
+ expires: expires,
1087
+ livemode: false,
1088
+ secret: "ek_test_default"
1089
+ }
1090
+ end
1030
1091
  end
1031
1092
  end
@@ -1,12 +1,13 @@
1
1
  module StripeMock
2
2
  module Data
3
3
  class List
4
- attr_reader :data, :limit, :offset, :starting_after
4
+ attr_reader :data, :limit, :offset, :starting_after, :ending_before
5
5
 
6
6
  def initialize(data, options = {})
7
7
  @data = Array(data.clone)
8
8
  @limit = [[options[:limit] || 10, 100].min, 1].max # restrict @limit to 1..100
9
9
  @starting_after = options[:starting_after]
10
+ @ending_before = options[:ending_before]
10
11
  if @data.first.is_a?(Hash) && @data.first[:created]
11
12
  @data.sort_by! { |x| x[:created] }
12
13
  @data.reverse!
@@ -46,9 +47,13 @@ module StripeMock
46
47
  private
47
48
 
48
49
  def offset
49
- if starting_after
50
+ case
51
+ when starting_after
50
52
  index = data.index { |datum| datum[:id] == starting_after }
51
53
  (index || raise("No such object id: #{starting_after}")) + 1
54
+ when ending_before
55
+ index = data.index { |datum| datum[:id] == ending_before }
56
+ (index || raise("No such object id: #{ending_before}")) - 1
52
57
  else
53
58
  0
54
59
  end
@@ -28,6 +28,7 @@ module StripeMock
28
28
  include StripeMock::RequestHandlers::Cards
29
29
  include StripeMock::RequestHandlers::Sources
30
30
  include StripeMock::RequestHandlers::Subscriptions # must be before Customers
31
+ include StripeMock::RequestHandlers::SubscriptionItems
31
32
  include StripeMock::RequestHandlers::Customers
32
33
  include StripeMock::RequestHandlers::Coupons
33
34
  include StripeMock::RequestHandlers::Disputes
@@ -36,16 +37,20 @@ module StripeMock
36
37
  include StripeMock::RequestHandlers::InvoiceItems
37
38
  include StripeMock::RequestHandlers::Orders
38
39
  include StripeMock::RequestHandlers::Plans
40
+ include StripeMock::RequestHandlers::Products
39
41
  include StripeMock::RequestHandlers::Refunds
40
42
  include StripeMock::RequestHandlers::Recipients
41
43
  include StripeMock::RequestHandlers::Transfers
42
44
  include StripeMock::RequestHandlers::Tokens
43
45
  include StripeMock::RequestHandlers::CountrySpec
44
46
  include StripeMock::RequestHandlers::Payouts
47
+ include StripeMock::RequestHandlers::EphemeralKey
48
+ include StripeMock::RequestHandlers::TaxRates
45
49
 
46
50
  attr_reader :accounts, :balance, :balance_transactions, :bank_tokens, :charges, :coupons, :customers,
47
51
  :disputes, :events, :invoices, :invoice_items, :orders, :plans, :recipients,
48
- :refunds, :transfers, :payouts, :subscriptions, :country_spec, :subscriptions_items
52
+ :refunds, :transfers, :payouts, :subscriptions, :country_spec, :subscriptions_items,
53
+ :products, :tax_rates
49
54
 
50
55
  attr_accessor :error_queue, :debug, :conversion_rate, :account_balance
51
56
 
@@ -64,13 +69,15 @@ module StripeMock
64
69
  @invoice_items = {}
65
70
  @orders = {}
66
71
  @plans = {}
72
+ @products = {}
67
73
  @recipients = {}
68
74
  @refunds = {}
69
75
  @transfers = {}
70
76
  @payouts = {}
71
77
  @subscriptions = {}
72
- @subscriptions_items = []
78
+ @subscriptions_items = {}
73
79
  @country_spec = {}
80
+ @tax_rates = {}
74
81
 
75
82
  @debug = false
76
83
  @error_queue = ErrorQueue.new
@@ -174,7 +181,8 @@ module StripeMock
174
181
  amount = params[:amount]
175
182
  unless amount.nil?
176
183
  # Fee calculation
177
- params[:fee] ||= (30 + (amount.abs * 0.029).ceil) * (amount > 0 ? 1 : -1)
184
+ calculate_fees(params) unless params[:fee]
185
+ params[:net] = amount - params[:fee]
178
186
  params[:amount] = amount * @conversion_rate
179
187
  end
180
188
  @balance_transactions[id] = Data.mock_balance_transaction(params.merge(id: id))
@@ -195,5 +203,31 @@ module StripeMock
195
203
  response = Struct.new(:data)
196
204
  response.new(hash)
197
205
  end
206
+
207
+ def calculate_fees(params)
208
+ application_fee = params[:application_fee] || 0
209
+ params[:fee] = processing_fee(params[:amount]) + application_fee
210
+ params[:fee_details] = [
211
+ {
212
+ amount: processing_fee(params[:amount]),
213
+ application: nil,
214
+ currency: params[:currency] || StripeMock.default_currency,
215
+ description: "Stripe processing fees",
216
+ type: "stripe_fee"
217
+ }
218
+ ]
219
+ if application_fee
220
+ params[:fee_details] << {
221
+ amount: application_fee,
222
+ currency: params[:currency] || StripeMock.default_currency,
223
+ description: "Application fee",
224
+ type: "application_fee"
225
+ }
226
+ end
227
+ end
228
+
229
+ def processing_fee(amount)
230
+ (30 + (amount.abs * 0.029).ceil) * (amount > 0 ? 1 : -1)
231
+ end
198
232
  end
199
233
  end
@@ -1,6 +1,7 @@
1
1
  module StripeMock
2
2
  module RequestHandlers
3
3
  module Accounts
4
+ VALID_START_YEAR = 2009
4
5
 
5
6
  def Accounts.included(klass)
6
7
  klass.add_handler 'post /v1/accounts', :new_account
@@ -30,6 +31,8 @@ module StripeMock
30
31
  account.merge!(params)
31
32
  if blank_value?(params[:tos_acceptance], :date)
32
33
  raise Stripe::InvalidRequestError.new("Invalid integer: ", "tos_acceptance[date]", http_status: 400)
34
+ elsif params[:tos_acceptance] && params[:tos_acceptance][:date]
35
+ validate_acceptance_date(params[:tos_acceptance][:date])
33
36
  end
34
37
  account
35
38
  end
@@ -65,6 +68,19 @@ module StripeMock
65
68
  end
66
69
  false
67
70
  end
71
+
72
+ def validate_acceptance_date(unix_date)
73
+ unix_now = Time.now.strftime("%s").to_i
74
+ formatted_date = Time.at(unix_date)
75
+
76
+ return if formatted_date.year >= VALID_START_YEAR && unix_now >= unix_date
77
+
78
+ raise Stripe::InvalidRequestError.new(
79
+ "ToS acceptance date is not valid. Dates are expected to be integers, measured in seconds, not in the future, and after 2009",
80
+ "tos_acceptance[date]",
81
+ http_status: 400
82
+ )
83
+ end
68
84
  end
69
85
  end
70
86
  end
@@ -13,9 +13,12 @@ module StripeMock
13
13
  end
14
14
 
15
15
  def new_charge(route, method_url, params, headers)
16
- if params[:idempotency_key] && charges.any?
17
- original_charge = charges.values.find { |c| c[:idempotency_key] == params[:idempotency_key]}
18
- return charges[original_charge[:id]] if original_charge
16
+ if headers && headers[:idempotency_key]
17
+ params[:idempotency_key] = headers[:idempotency_key]
18
+ if charges.any?
19
+ original_charge = charges.values.find { |c| c[:idempotency_key] == headers[:idempotency_key]}
20
+ return charges[original_charge[:id]] if original_charge
21
+ end
19
22
  end
20
23
 
21
24
  id = new_id('ch')
@@ -41,7 +44,7 @@ module StripeMock
41
44
  end
42
45
 
43
46
  ensure_required_params(params)
44
- bal_trans_params = { amount: params[:amount], source: id }
47
+ bal_trans_params = { amount: params[:amount], source: id, application_fee: params[:application_fee] }
45
48
 
46
49
  balance_transaction_id = new_balance_transaction('txn', bal_trans_params)
47
50
 
@@ -155,10 +158,6 @@ module StripeMock
155
158
  params[:amount] && params[:amount] < 1
156
159
  end
157
160
 
158
- def require_param(param)
159
- raise Stripe::InvalidRequestError.new("Missing required param: #{param}", param.to_s, http_status: 400)
160
- end
161
-
162
161
  def allowed_params(params)
163
162
  allowed = [:description, :metadata, :receipt_email, :fraud_details, :shipping, :destination]
164
163
 
@@ -51,7 +51,7 @@ module StripeMock
51
51
  coupon = coupons[ params[:coupon] ]
52
52
  assert_existence :coupon, params[:coupon], coupon
53
53
 
54
- add_coupon_to_customer(customers[params[:id]], coupon)
54
+ add_coupon_to_object(customers[params[:id]], coupon)
55
55
  end
56
56
 
57
57
  customers[ params[:id] ]
@@ -90,7 +90,7 @@ module StripeMock
90
90
  coupon = coupons[ params[:coupon] ]
91
91
  assert_existence :coupon, params[:coupon], coupon
92
92
 
93
- add_coupon_to_customer(cus, coupon)
93
+ add_coupon_to_object(cus, coupon)
94
94
  end
95
95
 
96
96
  cus