stall 0.1.1 → 0.1.2

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 (120) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -0
  3. data/app/assets/javascripts/stall.coffee +2 -0
  4. data/app/assets/javascripts/stall/cart-form.coffee +23 -0
  5. data/app/controllers/stall/application_controller.rb +10 -2
  6. data/app/controllers/stall/carts_controller.rb +31 -10
  7. data/app/controllers/stall/checkout/steps_controller.rb +36 -4
  8. data/app/controllers/stall/checkouts_controller.rb +12 -3
  9. data/app/controllers/stall/line_items_controller.rb +3 -3
  10. data/app/controllers/stall/payments_controller.rb +5 -3
  11. data/app/helpers/stall/add_to_cart_helper.rb +1 -1
  12. data/app/helpers/stall/checkout_helper.rb +22 -2
  13. data/app/helpers/stall/payments_helper.rb +8 -0
  14. data/app/mailers/stall/admin_mailer.rb +11 -0
  15. data/app/mailers/stall/base_mailer.rb +17 -0
  16. data/app/mailers/stall/customer_mailer.rb +13 -0
  17. data/app/models/address.rb +3 -0
  18. data/app/models/address_ownership.rb +3 -0
  19. data/app/models/adjustment.rb +3 -0
  20. data/app/models/cart.rb +3 -0
  21. data/app/models/customer.rb +3 -0
  22. data/app/models/line_item.rb +3 -0
  23. data/app/models/payment.rb +3 -0
  24. data/app/models/payment_method.rb +3 -0
  25. data/app/models/product_list.rb +3 -0
  26. data/app/models/shipment.rb +3 -0
  27. data/app/models/shipping_method.rb +3 -0
  28. data/app/models/stall/models.rb +4 -0
  29. data/app/models/stall/models/address.rb +45 -0
  30. data/app/models/stall/models/address_ownership.rb +26 -0
  31. data/app/models/stall/models/adjustment.rb +49 -0
  32. data/app/models/stall/models/cart.rb +37 -0
  33. data/app/models/stall/models/customer.rb +18 -0
  34. data/app/models/stall/models/line_item.rb +57 -0
  35. data/app/models/stall/models/payment.rb +33 -0
  36. data/app/models/stall/models/payment_method.rb +18 -0
  37. data/app/models/stall/models/product_list.rb +147 -0
  38. data/app/models/stall/models/shipment.rb +29 -0
  39. data/app/models/stall/models/shipping_method.rb +16 -0
  40. data/app/services/stall/add_to_cart_service.rb +11 -1
  41. data/app/services/stall/cart_update_service.rb +26 -0
  42. data/app/services/stall/payment_notification_service.rb +23 -9
  43. data/app/services/stall/shipping_fee_calculator_service.rb +13 -2
  44. data/app/views/checkout/steps/_informations.html.haml +3 -3
  45. data/app/views/checkout/steps/_payment.html.haml +25 -4
  46. data/app/views/checkout/steps/_payment_method.html.haml +1 -3
  47. data/app/views/checkout/steps/_payment_return.html.haml +6 -0
  48. data/app/views/checkout/steps/_shipping_method.html.haml +1 -1
  49. data/app/views/stall/admin_mailer/order_paid_email.html.haml +4 -0
  50. data/app/views/stall/carts/show.html.haml +36 -8
  51. data/app/views/stall/customer_mailer/order_paid_email.html.haml +8 -0
  52. data/app/views/stall/line_items/_added.html.haml +1 -1
  53. data/app/views/stall/shared/mailers/_address.html.haml +18 -0
  54. data/app/views/stall/shared/mailers/_cart.html.haml +114 -0
  55. data/config/locales/stall.fr.yml +90 -8
  56. data/db/migrate/20160304134849_change_all_json_columns_to_jsonb.rb +18 -0
  57. data/db/migrate/20160307142924_add_state_to_stall_addresses.rb +5 -0
  58. data/db/migrate/20160308142713_create_adjustments.rb +16 -0
  59. data/db/migrate/20160309165136_add_identifier_to_stall_product_lists.rb +5 -0
  60. data/db/migrate/20160316114649_add_data_to_stall_shipments.rb +5 -0
  61. data/db/migrate/20160317141632_add_state_to_stall_shipments.rb +5 -0
  62. data/db/migrate/20160629102943_add_active_to_stall_shipping_methods.rb +11 -0
  63. data/db/migrate/20160629104617_add_active_to_stall_payment_methods.rb +11 -0
  64. data/db/migrate/20160705110151_add_locale_to_stall_customers.rb +5 -0
  65. data/lib/generators/stall/checkout/step/templates/step.html.haml.erb +1 -1
  66. data/lib/generators/stall/checkout/step/templates/step.rb.erb +10 -2
  67. data/lib/generators/stall/checkout/wizard/templates/wizard.rb.erb +1 -1
  68. data/lib/generators/stall/install/templates/initializer.rb +85 -1
  69. data/lib/generators/stall/model/model_generator.rb +27 -0
  70. data/lib/generators/stall/service/service_generator.rb +39 -0
  71. data/lib/generators/stall/service/templates/service.rb.erb +16 -0
  72. data/lib/generators/stall/shipping/calculator/calculator_generator.rb +17 -0
  73. data/lib/generators/stall/shipping/calculator/templates/calculator.rb.erb +29 -0
  74. data/lib/stall.rb +4 -0
  75. data/lib/stall/addressable.rb +28 -2
  76. data/lib/stall/cart_helper.rb +84 -0
  77. data/lib/stall/carts_cleaner.rb +46 -0
  78. data/lib/stall/checkout.rb +2 -0
  79. data/lib/stall/checkout/informations_checkout_step.rb +3 -5
  80. data/lib/stall/checkout/payment_checkout_step.rb +3 -0
  81. data/lib/stall/checkout/payment_return_checkout_step.rb +11 -0
  82. data/lib/stall/checkout/shipping_method_checkout_step.rb +2 -1
  83. data/lib/stall/checkout/step.rb +47 -11
  84. data/lib/stall/checkout/step_form.rb +71 -0
  85. data/lib/stall/checkout/wizard.rb +15 -5
  86. data/lib/stall/config.rb +51 -0
  87. data/lib/stall/engine.rb +24 -2
  88. data/lib/stall/payable.rb +26 -0
  89. data/lib/stall/payments.rb +7 -3
  90. data/lib/stall/payments/config.rb +37 -0
  91. data/lib/stall/payments/fake_gateway_payment_notification.rb +34 -0
  92. data/lib/stall/payments/gateway.rb +19 -12
  93. data/lib/stall/payments/urls_config.rb +40 -0
  94. data/lib/stall/priceable.rb +7 -0
  95. data/lib/stall/rails/currency_helper.rb +1 -1
  96. data/lib/stall/routes.rb +17 -4
  97. data/lib/stall/sellable.rb +1 -2
  98. data/lib/stall/shipping.rb +3 -1
  99. data/lib/stall/shipping/calculator.rb +36 -3
  100. data/lib/stall/shipping/config.rb +17 -3
  101. data/lib/stall/shipping/country_weight_table_calculator.rb +1 -1
  102. data/lib/stall/shipping/free_shipping_calculator.rb +1 -1
  103. data/lib/stall/utils.rb +2 -2
  104. data/lib/stall/utils/config_dsl.rb +5 -1
  105. data/lib/stall/version.rb +1 -1
  106. data/lib/tasks/stall_tasks.rake +11 -0
  107. metadata +73 -15
  108. data/app/assets/javascripts/stall/application.js +0 -13
  109. data/app/assets/javascripts/stall/carts.js +0 -2
  110. data/app/helpers/stall/cart_helper.rb +0 -28
  111. data/app/models/stall/address.rb +0 -5
  112. data/app/models/stall/address_ownership.rb +0 -8
  113. data/app/models/stall/cart.rb +0 -31
  114. data/app/models/stall/customer.rb +0 -8
  115. data/app/models/stall/line_item.rb +0 -49
  116. data/app/models/stall/payment.rb +0 -14
  117. data/app/models/stall/payment_method.rb +0 -7
  118. data/app/models/stall/product_list.rb +0 -68
  119. data/app/models/stall/shipment.rb +0 -15
  120. data/app/models/stall/shipping_method.rb +0 -5
@@ -0,0 +1,16 @@
1
+ class <%= service_class_name %> < Stall::<%= service_class_name %>
2
+ # The call call method is the entry point for the service to process the data
3
+ # it was given at initialization
4
+ #
5
+ # Many overrides would want to call `super` to let the service do its
6
+ # classical job, then do something depending on the success or failure of
7
+ # the original work. This pattern is shown below.
8
+ #
9
+ # def call
10
+ # if super
11
+ # # Do something when it worked
12
+ # else
13
+ # # Do something when it didn't
14
+ # end
15
+ # end
16
+ end
@@ -0,0 +1,17 @@
1
+ module Stall
2
+ module Shipping
3
+ class CalculatorGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path('../templates', __FILE__)
5
+
6
+ def copy_calculator_template
7
+ template 'calculator.rb.erb', "lib/#{ file_path }.rb"
8
+ end
9
+
10
+ def register_calculator_in_initializer
11
+ insert_into_file "config/initializers/stall.rb", after: "Stall.configure do |config|\n" do
12
+ indent "config.shipping.register_calculator :#{ singular_name }, #{ class_name }\n"
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ class <%= class_name %> < Stall::Shipping::Calculator
2
+ # Use this method to handle the related shipping method availibility depending
3
+ # on the cart data available to the calculator
4
+ #
5
+ def available?
6
+ true
7
+ end
8
+
9
+ # Write your logic here and return the price for the current cart
10
+ #
11
+ # You can access `cart` and `shipping_method` (the model) here.
12
+ #
13
+ def price
14
+ 0
15
+ end
16
+
17
+ # Allows you to override how calculator's eot_price price is calculated
18
+ #
19
+ # def eot_price
20
+ # price / (1 + (vat_rate / 100.0))
21
+ # end
22
+
23
+ # Allows you to override the VAT for the calculator which allows calculating
24
+ # the EOT price
25
+ #
26
+ # def vat_rate
27
+ # Stall.config.vat_rate
28
+ # end
29
+ end
@@ -5,6 +5,7 @@ require 'request_store'
5
5
  require 'haml-rails'
6
6
  require 'simple_form'
7
7
  require 'country_select'
8
+ require 'cocoon'
8
9
 
9
10
  require 'stall/rails/routing_mapper'
10
11
  require 'stall/rails/currency_helper'
@@ -15,12 +16,15 @@ module Stall
15
16
 
16
17
  autoload :Sellable
17
18
  autoload :Addressable
19
+ autoload :Priceable
20
+ autoload :Payable
18
21
 
19
22
  autoload :Checkout
20
23
  autoload :Shipping
21
24
  autoload :Payments
22
25
 
23
26
  autoload :Routes
27
+ autoload :CartHelper
24
28
  autoload :Config
25
29
  autoload :Utils
26
30
 
@@ -6,7 +6,9 @@ module Stall
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  included do
9
- has_many :address_ownerships, as: :addressable, dependent: :destroy
9
+ has_many :address_ownerships, as: :addressable, dependent: :destroy,
10
+ inverse_of: :addressable
11
+
10
12
  has_many :addresses, through: :address_ownerships
11
13
  accepts_nested_attributes_for :address_ownerships, allow_destroy: true
12
14
  end
@@ -26,7 +28,16 @@ module Stall
26
28
  define_method(:"#{ type }_address=") do |address|
27
29
  ownership = address_ownership_for(type) || address_ownerships.build(type => true)
28
30
  ownership.address = address
29
- ownership.save if persisted?
31
+ instance_variable_set(instance_variable_name, address)
32
+ end
33
+
34
+ define_method(:"build_#{ type }_address") do |attributes = {}|
35
+ if (ownership = address_ownership_for(type))
36
+ ownership.send(:"#{ type }=", false)
37
+ end
38
+
39
+ ownership = address_ownerships.build(type => true)
40
+ address = ownership.build_address(attributes)
30
41
  instance_variable_set(instance_variable_name, address)
31
42
  end
32
43
 
@@ -39,6 +50,21 @@ module Stall
39
50
  instance_variable_set(instance_variable_name, ownership.address)
40
51
  end
41
52
  end
53
+
54
+ define_method(:"#{ type }_address_attributes=") do |attributes|
55
+ attributes.delete(:id)
56
+
57
+ ownership = address_ownership_for(type) || address_ownerships.build(type => true)
58
+ address = ownership.address || ownership.build_address
59
+ address.assign_attributes(attributes)
60
+ instance_variable_set(instance_variable_name, address)
61
+ end
62
+
63
+ define_method(:"mark_address_ownership_as_#{ type }") do |ownership|
64
+ ownership.send(:"mark_as_#{ type }").tap do
65
+ instance_variable_set(instance_variable_name, ownership.address)
66
+ end
67
+ end
42
68
  end
43
69
  end
44
70
  end
@@ -0,0 +1,84 @@
1
+ module Stall
2
+ module CartHelper
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ if respond_to?(:helper_method)
7
+ helper_method :current_cart, :current_cart_key
8
+ end
9
+
10
+ if respond_to?(:after_action)
11
+ after_action :store_cart_to_cookies
12
+ end
13
+ end
14
+
15
+ def current_cart
16
+ RequestStore.store[cart_key] ||= load_current_cart
17
+ end
18
+
19
+ protected
20
+
21
+ def current_cart_key
22
+ params[:cart_key].try(:to_sym) || :default
23
+ end
24
+
25
+ def load_current_cart(identifier = current_cart_key)
26
+ if (cart = find_cart(identifier))
27
+ return prepare_cart(cart)
28
+ end
29
+
30
+ # If no token was stored or the token does not exist anymore, create a
31
+ # new cart and store the new token
32
+ #
33
+ prepare_cart(cart_class.new(identifier: identifier))
34
+ end
35
+
36
+ def prepare_cart(cart)
37
+ cart.tap do |cart|
38
+ # Keep track of potential customer locale switching to allow e-mailing
39
+ # him in his last used locale
40
+ cart.customer.locale = I18n.locale if cart.customer
41
+
42
+ # Only update locale change for existing carts. New carts don't need
43
+ # to be saved, avoiding each robot or simple visitors to create a
44
+ # cart on large shops.
45
+ cart.save unless cart.new_record?
46
+ end
47
+ end
48
+
49
+ def find_cart(identifier)
50
+ if (cart_token = cookies.encrypted[cart_key(identifier)])
51
+ if (current_cart = ProductList.find_by_token(cart_token)) && current_cart.active?
52
+ return current_cart
53
+ else
54
+ # Remove any cart that can't be fetched, either because it's already
55
+ # paid, or because it was cleaned out
56
+ remove_cart_from_cookies(identifier)
57
+ nil
58
+ end
59
+ end
60
+ end
61
+
62
+ def remove_cart_from_cookies(identifier = current_cart_key)
63
+ cookies.delete(cart_key(identifier))
64
+ end
65
+
66
+ def cart_key(identifier = current_cart_key)
67
+ ['stall', 'cart', identifier.to_s].join('.')
68
+ end
69
+
70
+ def cart_class
71
+ Cart
72
+ end
73
+
74
+ def store_cart_to_cookies
75
+ if current_cart.persisted?
76
+ store_cart_cookie_for(current_cart.identifier, current_cart)
77
+ end
78
+ end
79
+
80
+ def store_cart_cookie_for(identifier, cart)
81
+ cookies.encrypted.permanent[cart_key(identifier)] = cart.token
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,46 @@
1
+ module Stall
2
+ class CartsCleaner
3
+ attr_reader :cart_model
4
+
5
+ def initialize(cart_model)
6
+ @cart_model = cart_model
7
+ end
8
+
9
+ def clean!
10
+ clean_empty_carts
11
+ clean_aborted_carts
12
+ end
13
+
14
+ private
15
+
16
+ # Empty carts are cleaned with a .delete_all call for optimization.
17
+ #
18
+ # Empty carts should not need to run destroy callbacks, not being related to
19
+ # any external model.
20
+ #
21
+ def clean_empty_carts
22
+ carts = cart_model.empty.older_than(Stall.config.empty_carts_expires_after.ago)
23
+
24
+ log "Cleaning #{ carts.count } empty carts ..."
25
+ carts.delete_all
26
+ log "Done."
27
+ end
28
+
29
+ # Unpaid carts have line items and other models related. Since empty carts
30
+ # are already cleaned, there should not be too many carts to destroy here
31
+ # and we can safely use .destroy_all to deeply clean carts.
32
+ #
33
+ # Note : The given cart model should implement the `.unpaid` method
34
+ def clean_aborted_carts
35
+ carts = cart_model.aborted(before: Stall.config.aborted_carts_expires_after.ago)
36
+
37
+ log "Cleaning #{ carts.count } aborted carts ..."
38
+ carts.destroy_all
39
+ log "Done."
40
+ end
41
+
42
+ def log(*args)
43
+ puts(*args) unless Rails.env.test?
44
+ end
45
+ end
46
+ end
@@ -4,11 +4,13 @@ module Stall
4
4
 
5
5
  autoload :Wizard
6
6
  autoload :Step
7
+ autoload :StepForm
7
8
 
8
9
  autoload :InformationsCheckoutStep
9
10
  autoload :ShippingMethodCheckoutStep
10
11
  autoload :PaymentMethodCheckoutStep
11
12
  autoload :PaymentCheckoutStep
13
+ autoload :PaymentReturnCheckoutStep
12
14
 
13
15
  class WizardNotFoundError < StandardError; end
14
16
  end
@@ -10,7 +10,7 @@ module Stall
10
10
  def process
11
11
  cart.assign_attributes(cart_params)
12
12
  process_addresses
13
- cart.save!
13
+ cart.save if valid?
14
14
  end
15
15
 
16
16
  private
@@ -20,10 +20,8 @@ module Stall
20
20
  end
21
21
 
22
22
  def ensure_address(type)
23
- unless cart.address_ownership_for(type)
24
- ownership = cart.address_ownerships.build(type => true)
25
- ownership.build_address
26
- end
23
+ ownership = cart.address_ownership_for(type) || cart.address_ownerships.build(type => true)
24
+ ownership.address || ownership.build_address
27
25
  end
28
26
 
29
27
  def process_addresses
@@ -1,6 +1,9 @@
1
1
  module Stall
2
2
  module Checkout
3
3
  class PaymentCheckoutStep < Stall::Checkout::Step
4
+ def process
5
+ return true if params[:succeeded]
6
+ end
4
7
  end
5
8
  end
6
9
  end
@@ -0,0 +1,11 @@
1
+ module Stall
2
+ module Checkout
3
+ class PaymentReturnCheckoutStep < Stall::Checkout::Step
4
+ include Stall::CartHelper
5
+
6
+ def prepare
7
+ remove_cart_from_cookies(cart.identifier)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -13,7 +13,8 @@ module Stall
13
13
  private
14
14
 
15
15
  def calculate_shipping_fee!
16
- Stall::ShippingFeeCalculatorService.new(cart).call
16
+ service_class = Stall.config.service_for(:shipping_fee_calculator)
17
+ service_class.new(cart).call
17
18
  end
18
19
  end
19
20
  end
@@ -3,11 +3,10 @@ module Stall
3
3
  class StepNotFoundError < StandardError; end
4
4
 
5
5
  class Step
6
- attr_reader :cart, :params
6
+ attr_reader :cart
7
7
 
8
- def initialize(cart, params)
8
+ def initialize(cart)
9
9
  @cart = cart
10
- @params = params
11
10
  end
12
11
 
13
12
  # Allow injecting dependencies on step initialization and accessing
@@ -25,32 +24,69 @@ module Stall
25
24
  end
26
25
 
27
26
  def process
28
- cart.update_attributes(cart_params)
27
+ save
29
28
  end
30
29
 
31
30
  def cart_params
32
- @cart_params ||= params.require(:cart).permit!
31
+ @cart_params ||= if params[:cart]
32
+ params.require(:cart).permit!
33
+ else
34
+ {}.with_indifferent_access
35
+ end
33
36
  end
34
37
 
35
38
  def skip?
36
39
  false
37
40
  end
38
41
 
42
+ def is?(key)
43
+ identifier == key
44
+ end
45
+
46
+ def identifier
47
+ @identifier ||= begin
48
+ class_name = self.class.name.demodulize
49
+ class_name.gsub(/CheckoutStep$/, '').underscore.to_sym
50
+ end
51
+ end
52
+
53
+ # Abstracts the simple case of assigning the submitted parameters to the
54
+ # cart object, running the step validations and saving the cart
55
+ def save
56
+ cart.assign_attributes(cart_params)
57
+ cart.save if valid?
58
+ end
59
+
39
60
  # Handles conversion from an identifier to a checkout step class, allowing
40
61
  # us to specify a list of symbols in our wizard's .step macro
41
62
  #
42
63
  def self.for(identifier)
43
64
  name = identifier.to_s.camelize
44
65
  step_name = [name, 'CheckoutStep'].join
66
+ # Try loading step from app
67
+ step = Stall::Utils.try_load_constant(step_name)
68
+ # Try loading step from stall core or lib if not found in app
69
+ step = Stall::Utils.try_load_constant(
70
+ ['Stall', 'Checkout', step_name.demodulize].join('::')
71
+ ) unless step
72
+
73
+ unless step
74
+ raise StepNotFoundError,
75
+ "No checkout step was found for #{ identifier }. You can generate " +
76
+ "it with `rails g stall:checkout:step #{ identifier }`"
77
+ end
45
78
 
46
- step = Stall::Utils.try_load_constant(step_name) ||
47
- Stall::Utils.try_load_constant(['Stall', 'Checkout', step_name.demodulize].join('::'))
79
+ step
80
+ end
48
81
 
49
- return step if step
82
+ def self.validations(&block)
83
+ return @validations unless block
84
+ @validations = Stall::Checkout::StepForm.build(&block)
85
+ end
50
86
 
51
- raise StepNotFoundError,
52
- "No checkout step was found for #{ identifier }. You can generate " +
53
- "it with `rails g stall:checkout:step #{ identifier }`"
87
+ def valid?
88
+ return true unless (validations = self.class.validations)
89
+ validations.new(cart, self).validate
54
90
  end
55
91
  end
56
92
  end
@@ -0,0 +1,71 @@
1
+ module Stall
2
+ module Checkout
3
+ class StepForm
4
+ include ActiveModel::Validations
5
+
6
+ class_attribute :nested_forms
7
+
8
+ attr_reader :object, :step
9
+
10
+ delegate :errors, to: :object
11
+
12
+ def initialize(object, step)
13
+ @object = object
14
+ @step = step
15
+ end
16
+
17
+ def validate
18
+ super && validate_nested_forms
19
+ end
20
+
21
+ def self.nested(type, &block)
22
+ self.nested_forms ||= {}
23
+ nested_forms[type] = build(&block)
24
+ end
25
+
26
+ def self.build(&block)
27
+ Class.new(StepForm, &block)
28
+ end
29
+
30
+ def method_missing(method, *args, &block)
31
+ if object.respond_to?(method, true)
32
+ object.send(method, *args, &block)
33
+ elsif step.respond_to?(method, true)
34
+ step.send(method, *args, &block)
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ # Override model name instanciation to add a name, since the form classes
41
+ # are anonymous, and ActiveModel::Name does not support unnamed classes
42
+ def model_name
43
+ @model_name ||= ActiveModel::Name.new(self, nil, object.class.name)
44
+ end
45
+
46
+ private
47
+
48
+ # Validates all registered nested forms
49
+ #
50
+ # Note : We use `forms.map.all?` instead if `forms.all?` to ensure
51
+ # all the validations are called and the iteration does not stop as soon
52
+ # as a validation fails
53
+ #
54
+ def validate_nested_forms
55
+ # If no nested forms are present in the class, just return true since
56
+ # no validation should be tested
57
+ return true unless self.class.nested_forms
58
+
59
+ # Run all validations on all nested forms and ensure they're all valid
60
+ self.class.nested_forms.map do |name, form|
61
+ if object.respond_to?(name) && (model = object.send(name))
62
+ Array.wrap(model).map { |m| form.new(m, step).validate }.all?
63
+ else
64
+ # Nested validations shouldn't be run on undefined relations
65
+ true
66
+ end
67
+ end.all?
68
+ end
69
+ end
70
+ end
71
+ end