solidus_related_products 1.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 (78) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.hound.yml +25 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +8 -0
  6. data/.travis.yml +15 -0
  7. data/CONTRIBUTING.md +81 -0
  8. data/Gemfile +6 -0
  9. data/Guardfile +10 -0
  10. data/LICENSE.md +26 -0
  11. data/README.md +80 -0
  12. data/Rakefile +15 -0
  13. data/app/assets/javascripts/spree/backend/solidus_related_products.js +1 -0
  14. data/app/assets/javascripts/spree/frontend/solidus_related_products.js +1 -0
  15. data/app/assets/stylesheets/spree/backend/solidus_related_products.css +3 -0
  16. data/app/assets/stylesheets/spree/frontend/solidus_related_products.css +3 -0
  17. data/app/controllers/spree/admin/products_controller_decorator.rb +10 -0
  18. data/app/controllers/spree/admin/relation_types_controller.rb +6 -0
  19. data/app/controllers/spree/admin/relations_controller.rb +80 -0
  20. data/app/controllers/spree/api/relations_controller.rb +78 -0
  21. data/app/models/spree/calculator/related_product_discount.rb +50 -0
  22. data/app/models/spree/product_decorator.rb +106 -0
  23. data/app/models/spree/relation.rb +7 -0
  24. data/app/models/spree/relation_type.rb +6 -0
  25. data/app/overrides/add_product_relation_admin_sub_menu_tab.rb +6 -0
  26. data/app/overrides/add_related_product_admin_tabs.rb +6 -0
  27. data/app/views/spree/admin/products/_related_products.html.erb +3 -0
  28. data/app/views/spree/admin/products/_related_products_table.html.erb +42 -0
  29. data/app/views/spree/admin/products/related.html.erb +70 -0
  30. data/app/views/spree/admin/relation_types/_form.html.erb +19 -0
  31. data/app/views/spree/admin/relation_types/edit.html.erb +14 -0
  32. data/app/views/spree/admin/relation_types/index.html.erb +44 -0
  33. data/app/views/spree/admin/relation_types/new.html.erb +14 -0
  34. data/app/views/spree/admin/relations/create.js.erb +5 -0
  35. data/app/views/spree/admin/relations/destroy.js.erb +1 -0
  36. data/app/views/spree/api/relations/show.v1.rabl +2 -0
  37. data/bin/rails +7 -0
  38. data/config/locales/cs.yml +18 -0
  39. data/config/locales/de.yml +18 -0
  40. data/config/locales/en.yml +18 -0
  41. data/config/locales/es.yml +18 -0
  42. data/config/locales/fr.yml +18 -0
  43. data/config/locales/it.yml +18 -0
  44. data/config/locales/nl.yml +18 -0
  45. data/config/locales/pl.yml +18 -0
  46. data/config/locales/pt-BR.yml +18 -0
  47. data/config/locales/ru.yml +18 -0
  48. data/config/locales/sv.yml +18 -0
  49. data/config/routes.rb +24 -0
  50. data/db/migrate/20100308090631_create_relation_types.rb +14 -0
  51. data/db/migrate/20100308092101_create_relations.rb +14 -0
  52. data/db/migrate/20100324123835_add_discount_to_relation.rb +9 -0
  53. data/db/migrate/20111129044813_prefixing_tables_with_spree.rb +6 -0
  54. data/db/migrate/20120208144454_update_relation_types.rb +9 -0
  55. data/db/migrate/20120623014337_update_relations.rb +11 -0
  56. data/db/migrate/20130727004612_add_position_to_spree_relations.rb +5 -0
  57. data/lib/generators/solidus_related_products/install/install_generator.rb +20 -0
  58. data/lib/solidus_related_products.rb +7 -0
  59. data/lib/solidus_related_products/engine.rb +24 -0
  60. data/lib/solidus_related_products/version.rb +18 -0
  61. data/solidus_related_products.gemspec +45 -0
  62. data/spec/controllers/spree/admin/products_controller_decorator_spec.rb +20 -0
  63. data/spec/controllers/spree/admin/relations_controller_spec.rb +96 -0
  64. data/spec/controllers/spree/api/relations_controller_spec.rb +98 -0
  65. data/spec/factories/relation_factory.rb +7 -0
  66. data/spec/factories/relation_type_factory.rb +6 -0
  67. data/spec/features/spree/admin/product_relation_spec.rb +86 -0
  68. data/spec/features/spree/admin/relation_types_spec.rb +97 -0
  69. data/spec/models/spree/calculator/related_product_discount_spec.rb +48 -0
  70. data/spec/models/spree/product_spec.rb +129 -0
  71. data/spec/models/spree/relation_spec.rb +13 -0
  72. data/spec/models/spree/relation_type_spec.rb +18 -0
  73. data/spec/spec_helper.rb +48 -0
  74. data/spec/support/capybara.rb +20 -0
  75. data/spec/support/database_cleaner.rb +23 -0
  76. data/spec/support/factory_girl.rb +7 -0
  77. data/spec/support/spree.rb +18 -0
  78. metadata +388 -0
@@ -0,0 +1,96 @@
1
+ RSpec.describe Spree::Admin::RelationsController, type: :controller do
2
+ stub_authorization!
3
+
4
+ let(:user) { create(:user) }
5
+ let!(:product) { create(:product) }
6
+ let!(:other1) { create(:product) }
7
+
8
+ let!(:relation_type) { create(:relation_type) }
9
+ let!(:relation) do
10
+ create(
11
+ :relation,
12
+ relatable: product,
13
+ related_to: other1,
14
+ relation_type: relation_type,
15
+ position: 0
16
+ )
17
+ end
18
+
19
+ before { stub_authentication! }
20
+ after { Spree::Admin::ProductsController.clear_overrides! }
21
+
22
+ context '.model_class' do
23
+ it 'responds to model_class as Spree::Relation' do
24
+ expect(controller.send(:model_class)).to eq Spree::Relation
25
+ end
26
+ end
27
+
28
+ describe 'with JS' do
29
+ sign_in_as_admin!
30
+
31
+ let(:valid_params) do
32
+ {
33
+ format: :js,
34
+ product_id: product.id,
35
+ relation: {
36
+ related_to_id: other1.id,
37
+ relation_type_id: relation_type.id
38
+ }
39
+ }
40
+ end
41
+
42
+ context '#create' do
43
+ it 'is not routable' do
44
+ spree_post :create, valid_params
45
+ expect(response.status).to be(200)
46
+ end
47
+
48
+ it 'returns success with valid params' do
49
+ expect {
50
+ spree_post :create, valid_params
51
+ }.to change(Spree::Relation, :count).by(1)
52
+ end
53
+
54
+ it 'raises error with invalid params' do
55
+ expect {
56
+ spree_post :create, format: :js
57
+ }.to raise_error
58
+ end
59
+ end
60
+
61
+ context '#update' do
62
+ it 'redirects to product/related url' do
63
+ spree_put :update, product_id: product.id, id: 1, relation: { discount_amount: 2.0 }
64
+ expect(response).to redirect_to(spree.admin_product_path(relation.relatable) + '/related')
65
+ end
66
+ end
67
+
68
+ context '#destroy' do
69
+ it 'records successfully' do
70
+ expect {
71
+ spree_delete :destroy, id: 1, product_id: product.id, format: :js
72
+ }.to change(Spree::Relation, :count).by(-1)
73
+ end
74
+ end
75
+
76
+ context '#update_positions' do
77
+ it 'returns the correct position of the related products' do
78
+ other2 = create(:product)
79
+ relation2 = create(
80
+ :relation, relatable: product, related_to: other2, relation_type: relation_type, position: 1
81
+ )
82
+
83
+ expect {
84
+ params = {
85
+ product_id: product.id,
86
+ id: relation.id,
87
+ positions: { relation.id => '1', relation2.id => '0' },
88
+ format: :js
89
+ }
90
+ spree_post :update_positions, params
91
+ relation.reload
92
+ }.to change(relation, :position).from(0).to(1)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,98 @@
1
+ RSpec.describe Spree::Api::RelationsController, type: :controller do
2
+ stub_authorization!
3
+ render_views
4
+
5
+ let(:user) { create(:user) }
6
+ let!(:product) { create(:product) }
7
+ let!(:other1) { create(:product) }
8
+
9
+ let!(:relation_type) { create(:relation_type) }
10
+ let!(:relation) do
11
+ create(
12
+ :relation,
13
+ relatable: product,
14
+ related_to: other1,
15
+ relation_type: relation_type,
16
+ position: 0
17
+ )
18
+ end
19
+
20
+ before { stub_authentication! }
21
+ after { Spree::Admin::ProductsController.clear_overrides! }
22
+
23
+ context 'model_class' do
24
+ it 'responds to model_class as Spree::Relation' do
25
+ expect(controller.send(:model_class)).to eq Spree::Relation
26
+ end
27
+ end
28
+
29
+ describe 'with JSON' do
30
+ sign_in_as_admin!
31
+
32
+ let(:valid_params) do
33
+ {
34
+ format: :json,
35
+ product_id: product.id,
36
+ relation: {
37
+ related_to_id: other1.id,
38
+ relation_type_id: relation_type.id
39
+ },
40
+ token: user.spree_api_key
41
+ }
42
+ end
43
+
44
+ context '#create' do
45
+ it 'creates the relation' do
46
+ spree_post :create, valid_params
47
+ expect(response.status).to eq(201)
48
+ end
49
+
50
+ it 'responds 422 error with invalid params' do
51
+ spree_post :create, format: :json, product_id: product.id, token: user.spree_api_key
52
+ expect(response.status).to eq(422)
53
+ end
54
+ end
55
+
56
+ context '#update' do
57
+ it 'succesfully updates the relation ' do
58
+ params = {
59
+ format: :json,
60
+ product_id: product.id,
61
+ id: relation.id,
62
+ relation: { discount_amount: 2.0 }
63
+ }
64
+ expect {
65
+ spree_put :update, params
66
+ }.to change { relation.reload.discount_amount.to_s }.from('0.0').to('2.0')
67
+ end
68
+ end
69
+
70
+ context '#destroy with' do
71
+ it 'records successfully' do
72
+ expect {
73
+ spree_delete :destroy, id: 1, product_id: product.id, format: :json, token: user.spree_api_key
74
+ }.to change(Spree::Relation, :count).by(-1)
75
+ end
76
+ end
77
+
78
+ context '#update_positions' do
79
+ it 'returns the correct position of the related products' do
80
+ other2 = create(:product)
81
+ relation2 = create(
82
+ :relation, relatable: product, related_to: other2, relation_type: relation_type, position: 1
83
+ )
84
+
85
+ expect {
86
+ params = {
87
+ product_id: product.id,
88
+ id: relation.id,
89
+ positions: { relation.id => '1', relation2.id => '0' },
90
+ format: :json
91
+ }
92
+ spree_post :update_positions, params
93
+ relation.reload
94
+ }.to change(relation, :position).from(0).to(1)
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,7 @@
1
+ FactoryGirl.define do
2
+ factory :relation, class: Spree::Relation do
3
+ association :relatable, factory: :product
4
+ association :related_to, factory: :product
5
+ relation_type 'Spree::Product'
6
+ end
7
+ end
@@ -0,0 +1,6 @@
1
+ FactoryGirl.define do
2
+ factory :relation_type, class: Spree::RelationType do
3
+ name { generate(:random_string) }
4
+ applies_to 'Spree::Product'
5
+ end
6
+ end
@@ -0,0 +1,86 @@
1
+ RSpec.feature 'Admin Product Relation', :js do
2
+ stub_authorization!
3
+
4
+ given!(:product) { create(:product) }
5
+ given!(:other) { create(:product) }
6
+
7
+ given!(:relation_type) { create(:relation_type, name: 'Gears') }
8
+
9
+ background do
10
+ visit spree.edit_admin_product_path(product)
11
+ click_link 'Related Products'
12
+ end
13
+
14
+ scenario 'create relation' do
15
+ expect(page).to have_text 'ADD RELATED PRODUCT'
16
+ expect(page).to have_text product.name
17
+
18
+ within('#add-line-item') do
19
+ select2_search other.name, from: 'Name or SKU'
20
+ select2_search relation_type.name, from: 'Type'
21
+ fill_in 'add_discount', with: '0.8'
22
+ click_link 'Add'
23
+ end
24
+
25
+ wait_for_ajax
26
+
27
+ within_row(1) do
28
+ expect(page).to have_field('relation_discount_amount', with: '0.8')
29
+ expect(column_text(2)).to eq other.name
30
+ expect(column_text(3)).to eq relation_type.name
31
+ end
32
+ end
33
+
34
+ context 'with relations' do
35
+ given!(:relation) do
36
+ create(
37
+ :relation,
38
+ relatable: product,
39
+ related_to: other,
40
+ relation_type: relation_type,
41
+ discount_amount: 0.5
42
+ )
43
+ end
44
+
45
+ background do
46
+ visit spree.edit_admin_product_path(product)
47
+ click_link 'Related Products'
48
+ end
49
+
50
+ scenario 'ensure content exist' do
51
+ expect(page).to have_text 'ADD RELATED PRODUCT'
52
+ expect(page).to have_text product.name
53
+ expect(page).to have_text other.name
54
+
55
+ within_row(1) do
56
+ expect(page).to have_field('relation_discount_amount', with: '0.5')
57
+ expect(column_text(2)).to eq other.name
58
+ expect(column_text(3)).to eq relation_type.name
59
+ end
60
+ end
61
+
62
+ scenario 'update discount' do
63
+ within_row(1) do
64
+ fill_in 'relation_discount_amount', with: '0.9'
65
+ click_on 'Update'
66
+ end
67
+ wait_for_ajax
68
+ within_row(1) do
69
+ expect(page).to have_field('relation_discount_amount', with: '0.9')
70
+ end
71
+ end
72
+
73
+ context 'delete' do
74
+ scenario 'can remove records' do
75
+ expect(page).to have_text other.name
76
+ within_row(1) do
77
+ expect(column_text(2)).to eq other.name
78
+ click_icon :trash
79
+ end
80
+ page.driver.browser.switch_to.alert.accept unless Capybara.javascript_driver == :poltergeist
81
+ wait_for_ajax
82
+ expect(page).not_to have_text other.name
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,97 @@
1
+ RSpec.feature 'Admin Relation Types', :js do
2
+ stub_authorization!
3
+
4
+ background do
5
+ visit spree.admin_relation_types_path
6
+ end
7
+
8
+ scenario 'when no relation types exists' do
9
+ expect(page).to have_text 'NO RELATION TYPES FOUND, ADD ONE!'
10
+ end
11
+
12
+ context 'create' do
13
+ scenario 'can create a new relation type' do
14
+ click_link 'New Relation Type'
15
+ expect(current_path).to eq spree.new_admin_relation_type_path
16
+
17
+ fill_in 'Name', with: 'Gears'
18
+ fill_in 'Applies To', with: 'Spree:Products'
19
+
20
+ click_button 'Create'
21
+
22
+ expect(page).to have_text 'successfully created!'
23
+ expect(current_path).to eq spree.admin_relation_types_path
24
+ end
25
+
26
+ scenario 'shows validation errors with blank :name' do
27
+ click_link 'New Relation Type'
28
+ expect(current_path).to eq spree.new_admin_relation_type_path
29
+
30
+ fill_in 'Name', with: ''
31
+ click_button 'Create'
32
+
33
+ expect(page).to have_text 'Name can\'t be blank'
34
+ end
35
+
36
+ scenario 'shows validation errors with blank :applies_to' do
37
+ click_link 'New Relation Type'
38
+ expect(current_path).to eq spree.new_admin_relation_type_path
39
+
40
+ fill_in 'Name', with: 'Gears'
41
+ fill_in 'Applies To', with: ''
42
+ click_button 'Create'
43
+
44
+ expect(page).to have_text 'Applies to can\'t be blank'
45
+ end
46
+ end
47
+
48
+ context 'with records' do
49
+ background do
50
+ %w(Gears Equipments).each do |name|
51
+ create(:relation_type, name: name)
52
+ end
53
+ visit spree.admin_relation_types_path
54
+ end
55
+
56
+ context 'show' do
57
+ scenario 'displays existing relation types' do
58
+ within_row(1) do
59
+ expect(column_text(1)).to eq 'Gears'
60
+ expect(column_text(2)).to eq 'Spree::Product'
61
+ expect(column_text(3)).to eq ''
62
+ end
63
+ end
64
+ end
65
+
66
+ context 'edit' do
67
+ background do
68
+ within_row(1) { click_icon :edit }
69
+ expect(current_path).to eq spree.edit_admin_relation_type_path(1)
70
+ end
71
+
72
+ scenario 'can update an existing relation type' do
73
+ fill_in 'Name', with: 'Gadgets'
74
+ click_button 'Update'
75
+ expect(page).to have_text 'successfully updated!'
76
+ expect(page).to have_text 'Gadgets'
77
+ end
78
+
79
+ scenario 'shows validation errors with blank :name' do
80
+ fill_in 'Name', with: ''
81
+ click_button 'Update'
82
+ expect(page).to have_text 'Name can\'t be blank'
83
+ end
84
+ end
85
+
86
+ context 'delete' do
87
+ scenario 'can remove records' do
88
+ within_row(1) do
89
+ expect(column_text(1)).to eq 'Gears'
90
+ click_icon :trash
91
+ end
92
+ page.driver.browser.switch_to.alert.accept unless Capybara.javascript_driver == :poltergeist
93
+ expect(page).to have_text 'successfully removed!'
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,48 @@
1
+ RSpec.describe Spree::Calculator::RelatedProductDiscount, type: :model do
2
+ subject { described_class.new }
3
+
4
+ context '.description' do
5
+ it 'outputs relation product discount' do
6
+ expect(subject.description).to eq Spree.t(:related_product_discount)
7
+ end
8
+ end
9
+
10
+ describe '.compute(object)' do
11
+ it 'returns nil with empty Array' do
12
+ expect(subject.compute([])).to be_nil
13
+ end
14
+
15
+ it 'returns nil unless order is eligible' do
16
+ empty_order = double('Spree::Order')
17
+ allow(empty_order).to receive(:line_items).and_return([])
18
+ expect(subject.compute(empty_order)).to be_nil
19
+ end
20
+
21
+ context 'with order' do
22
+ before do
23
+ @order = double('Spree::Order')
24
+ product = build_stubbed(:product)
25
+ variant = double('Spree::Variant', product: product)
26
+ price = double('Spree::Price', variant: variant, amount: 5.00)
27
+ line_item = double('Spree::LineItem', variant: variant, order: @order, quantity: 1, price: 4.99)
28
+
29
+ allow(variant).to receive(:default_price).and_return(price)
30
+ allow(@order).to receive(:line_items).and_return([line_item])
31
+
32
+ related_product = create(:product)
33
+ relation_type = create(:relation_type)
34
+
35
+ create(:relation, relatable: product, related_to: related_product, relation_type: relation_type, discount_amount: 1.0)
36
+ end
37
+
38
+ it 'returns total count of Array' do
39
+ objects = Array.new { @order }
40
+ expect(subject.compute(objects)).to be_nil
41
+ end
42
+
43
+ it 'returns total count' do
44
+ expect(subject.compute(@order)).to be_zero
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,129 @@
1
+ RSpec.describe Spree::Product, type: :model do
2
+ context 'class' do
3
+ describe '.relation_types' do
4
+ it 'returns all the RelationTypes in use for this Product' do
5
+ relation_type = create(:relation_type)
6
+ expect(described_class.relation_types).to include(relation_type)
7
+ end
8
+ end
9
+ end
10
+
11
+ context 'relations' do
12
+ it { is_expected.to have_many(:relations) }
13
+ end
14
+
15
+ context 'instance' do
16
+ let(:other1) { create(:product) }
17
+ let(:other2) { create(:product) }
18
+
19
+ before do
20
+ @product = create(:product)
21
+ @relation_type = create(:relation_type, name: 'Related Products')
22
+ end
23
+
24
+ describe '.relations' do
25
+ it 'has many relations' do
26
+ relation1 = create(:relation, relatable: @product, related_to: other1, relation_type: @relation_type)
27
+ relation2 = create(:relation, relatable: @product, related_to: other2, relation_type: @relation_type)
28
+
29
+ @product.reload
30
+ expect(@product.relations).to include(relation1)
31
+ expect(@product.relations).to include(relation2)
32
+ end
33
+
34
+ it 'has many relations for different RelationTypes' do
35
+ other_relation_type = Spree::RelationType.new(name: 'Recommended Products')
36
+
37
+ relation1 = create(:relation, relatable: @product, related_to: other1, relation_type: @relation_type)
38
+ relation2 = create(:relation, relatable: @product, related_to: other1, relation_type: other_relation_type)
39
+
40
+ @product.reload
41
+ expect(@product.relations).to include(relation1)
42
+ expect(@product.relations).to include(relation2)
43
+ end
44
+ end
45
+
46
+ describe 'RelationType finders' do
47
+ before do
48
+ @relation = create(:relation, relatable: @product, related_to: other1, relation_type: @relation_type)
49
+ @product.reload
50
+ end
51
+
52
+ it 'returns the relevant relations' do
53
+ expect(@product.related_products).to include(other1)
54
+ end
55
+
56
+ it 'recognizes the method with has_related_products?(method)' do
57
+ expect(@product.has_related_products?('related_products')).to be_truthy
58
+ end
59
+
60
+ it 'does not recognize non-existent methods with has_related_products?(method)' do
61
+ expect(@product.has_related_products?('unrelated_products')).not_to be_truthy
62
+ end
63
+
64
+ it 'is the pluralised form of the RelationType name' do
65
+ @relation_type.update_attributes(name: 'Related Product')
66
+ expect(@product.related_products).to include(other1)
67
+ end
68
+
69
+ it 'does not return relations for another RelationType' do
70
+ other_relation_type = Spree::RelationType.new(name: 'Recommended Products')
71
+
72
+ create(:relation, relatable: @product, related_to: other1, relation_type: @relation_type)
73
+ create(:relation, relatable: @product, related_to: other2, relation_type: other_relation_type)
74
+
75
+ @product.reload
76
+ expect(@product.related_products).to include(other1)
77
+ expect(@product.related_products).not_to include(other2)
78
+ end
79
+
80
+ it 'does not return Products that are deleted' do
81
+ other1.update_attributes(deleted_at: Time.now)
82
+ expect(@product.related_products).to be_blank
83
+ end
84
+
85
+ it 'does not return Products that are not yet available' do
86
+ other1.update_attributes(available_on: Time.now + 1.hour)
87
+ expect(@product.related_products).to be_blank
88
+ end
89
+
90
+ it 'does not return Products where available_on are blank' do
91
+ other1.update_attributes(available_on: nil)
92
+ expect(@product.related_products).to be_blank
93
+ end
94
+
95
+ it 'returns all results when .relation_filter is nil' do
96
+ expect(described_class).to receive(:relation_filter).and_return(nil)
97
+ other1.update_attributes(available_on: Time.now + 1.hour)
98
+ expect(@product.related_products).to include(other1)
99
+ end
100
+
101
+ context 'with an enhanced Product.relation_filter' do
102
+ it 'restricts the filter' do
103
+ relation_filter = described_class.relation_filter
104
+ expect(described_class).to receive(:relation_filter).at_least(:once).and_return(relation_filter.includes(:master).where('spree_variants.cost_price > 20'))
105
+
106
+ other1.master.update_attributes(cost_price: 10)
107
+ other2.master.update_attributes(cost_price: 30)
108
+
109
+ create(:relation, relatable: @product, related_to: other2, relation_type: @relation_type)
110
+ results = @product.related_products
111
+ expect(results).not_to include(other1)
112
+ expect(results).to include(other2)
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ context 'instance when relation_types table is missing' do
119
+ it 'method missing should not throw ActiveRecord::StatementInvalid when the spree_relation_types table is missing' do
120
+ described_class.connection.rename_table('spree_relation_types', 'missing_relation_types')
121
+ begin
122
+ product = described_class.new
123
+ expect { product.foo }.to raise_error(NameError)
124
+ ensure
125
+ described_class.connection.rename_table('missing_relation_types', 'spree_relation_types')
126
+ end
127
+ end
128
+ end
129
+ end