spree_abandoned_cart 0.3.0 → 0.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +17 -0
- data/.gitlab-ci.yml +15 -0
- data/.rspec +1 -0
- data/Gemfile +8 -0
- data/LICENSE +26 -0
- data/README.md +69 -0
- data/Rakefile +15 -0
- data/app/controllers/spree/admin/abandoned_orders_controller.rb +35 -0
- data/app/mailers/spree/abandoned_cart_mailer.rb +20 -0
- data/app/models/order_decorator.rb +38 -0
- data/app/models/order_updater_decorator.rb +25 -0
- data/app/models/spree/abandoned_cart_email_configuration.rb +5 -0
- data/app/models/spree/abandoned_order.rb +30 -0
- data/app/overrides/add_button_to_orders_tab_for_abandoned_index_page.rb +8 -0
- data/app/views/spree/abandoned_cart_mailer/abandoned_email.html.erb +145 -0
- data/app/views/spree/abandoned_cart_mailer/abandoned_second_email.html.erb +145 -0
- data/app/views/spree/abandoned_cart_mailer/abandoned_third_email.html.erb +145 -0
- data/app/views/spree/admin/abandoned_orders/_filters_bar.html.haml +49 -0
- data/app/views/spree/admin/abandoned_orders/_list.html.haml +22 -0
- data/app/views/spree/admin/abandoned_orders/_list_respond.js.erb +1 -0
- data/app/views/spree/admin/abandoned_orders/_order_summary.html.haml +16 -0
- data/app/views/spree/admin/abandoned_orders/_titles.html.haml +6 -0
- data/app/views/spree/admin/abandoned_orders/index.html.haml +3 -0
- data/app/views/spree/admin/abandoned_orders/index.js.haml +1 -0
- data/app/views/spree/admin/abandoned_orders/show.html.haml +2 -0
- data/config/initializers/load_preferences.rb +5 -0
- data/config/locales/en.yml +17 -0
- data/config/locales/ru.yml +17 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20130722151112_add_abandoned_email_sent_at_to_spree_orders.rb +5 -0
- data/db/migrate/20150825122125_create_spree_abandoned_orders.rb +12 -0
- data/lib/generators/spree_abandoned_cart/install/install_generator.rb +21 -0
- data/lib/spree_abandoned_cart.rb +3 -0
- data/lib/spree_abandoned_cart/engine.rb +22 -0
- data/lib/spree_abandoned_cart/factories.rb +6 -0
- data/script/rails +7 -0
- data/spec/models/order_spec.rb +35 -0
- data/spec/spec_helper.rb +86 -0
- data/spree_abandoned_cart.gemspec +38 -0
- metadata +68 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f4079aa9bcd323157d4a9066b95c195d3f7ad11
|
4
|
+
data.tar.gz: 5cdbf279a566c2a57829539d77a2c02f02618c9b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d6cd065fa913f3e49bfd3ef90258606d2d004ca574d0bc43872a2e5e288f712bd5e3f0be7e99f38b9d570762febc1135afabf5a65662fdf4673a02f21211721
|
7
|
+
data.tar.gz: 8863266e96146919e7987b2295400c2e0854ce20d937e81cb3126b1102b99171753c1fb107014f7bd2a9d0d980ec653c0038c6ed8887f907c4ffbe4305620934
|
data/.gitignore
ADDED
data/.gitlab-ci.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
image: ruby:2.4.2
|
2
|
+
|
3
|
+
cache:
|
4
|
+
paths:
|
5
|
+
- cache
|
6
|
+
|
7
|
+
before_script:
|
8
|
+
- apt-get update -q && apt-get install nodejs -yqq
|
9
|
+
- gem install bundler -v 1.15.4 --no-ri --no-rdoc
|
10
|
+
- bundle install -j $(nproc) --path cache
|
11
|
+
|
12
|
+
test:
|
13
|
+
script:
|
14
|
+
- bundle exec rake test_app
|
15
|
+
- rspec spec
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
Copyright (c) 2013 [name of plugin creator]
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without modification,
|
5
|
+
are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright notice,
|
8
|
+
this list of conditions and the following disclaimer.
|
9
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
11
|
+
and/or other materials provided with the distribution.
|
12
|
+
* Neither the name Spree nor the names of its contributors may be used to
|
13
|
+
endorse or promote products derived from this software without specific
|
14
|
+
prior written permission.
|
15
|
+
|
16
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
17
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
18
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
19
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
20
|
+
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
21
|
+
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
22
|
+
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
23
|
+
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
24
|
+
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
25
|
+
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
26
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
## SpreeAbandonedCartEmail
|
2
|
+
|
3
|
+
Abandoned cart email extension. Looks for orders with line items that were created over 12 hours ago and gives them a nudge to complete.
|
4
|
+
|
5
|
+
Spree Commerce versions:
|
6
|
+
|
7
|
+
- Spree Core - 3.6
|
8
|
+
- Spree Auth Devise - 3.3
|
9
|
+
|
10
|
+
Use the config values below to change settings:
|
11
|
+
|
12
|
+
```
|
13
|
+
Spree::AbandonedCartEmailConfig::Config.email_timeframe_from, :integer
|
14
|
+
Spree::AbandonedCartEmailConfig::Config.email_timeframe_to, :integer
|
15
|
+
|
16
|
+
Spree::AbandonedCartEmail::Config.email_from = "spree@example.com"
|
17
|
+
```
|
18
|
+
|
19
|
+
Override the email view to customise:
|
20
|
+
|
21
|
+
```
|
22
|
+
app/views/spree/abandoned_cart_mailer/abandoned_email.html.erb
|
23
|
+
```
|
24
|
+
|
25
|
+
Create a rake task to send the email:
|
26
|
+
|
27
|
+
```
|
28
|
+
desc "Abandoned cart emails"
|
29
|
+
task send_abandond_cart: :environment do
|
30
|
+
Spree::Order.email_eligible_abandoned_email_orders
|
31
|
+
end
|
32
|
+
```
|
33
|
+
Create a cronjob to run every so often to send the emails:
|
34
|
+
|
35
|
+
```
|
36
|
+
0 * * * * /bin/bash -l -c 'cd {your_application_home} && RAILS_ENV=production rake send_abandoned_cart 2>&1'
|
37
|
+
```
|
38
|
+
|
39
|
+
## Installation
|
40
|
+
|
41
|
+
Add spree_abandoned_cart to your Gemfile:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
gem 'spree_abandoned_cart'
|
45
|
+
```
|
46
|
+
|
47
|
+
Bundle your dependencies and run the installation generator:
|
48
|
+
|
49
|
+
```shell
|
50
|
+
bundle
|
51
|
+
bundle exec rails g spree_abandoned_cart:install
|
52
|
+
```
|
53
|
+
|
54
|
+
## Testing
|
55
|
+
|
56
|
+
Be sure to bundle your dependencies and then create a dummy test app for the specs to run against.
|
57
|
+
|
58
|
+
```shell
|
59
|
+
bundle
|
60
|
+
bundle exec rake test_app
|
61
|
+
bundle exec rspec spec
|
62
|
+
```
|
63
|
+
|
64
|
+
When testing your applications integration with this extension you may use it's factories.
|
65
|
+
Simply add this require statement to your spec_helper:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
require 'spree_abandoned_cart/factories'
|
69
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'spree/testing_support/extension_rake'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new
|
8
|
+
|
9
|
+
task :default => [:spec]
|
10
|
+
|
11
|
+
desc 'Generates a dummy app for testing'
|
12
|
+
task :test_app do
|
13
|
+
ENV['LIB_NAME'] = 'spree_abandoned_cart'
|
14
|
+
Rake::Task['extension:test_app'].invoke
|
15
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Spree::Admin::AbandonedOrdersController < Spree::Admin::BaseController
|
2
|
+
before_action :find_abandoned_cart, except: [:index ]
|
3
|
+
respond_to :js, only: [:index]
|
4
|
+
|
5
|
+
def index
|
6
|
+
params[:q] ||= {}
|
7
|
+
|
8
|
+
params_clone = params[:q].deep_dup
|
9
|
+
|
10
|
+
if params_clone[:created_at_gt].present?
|
11
|
+
params_clone[:created_at_gt] = Time.zone.parse(params_clone[:created_at_gt]).beginning_of_day rescue ''
|
12
|
+
end
|
13
|
+
|
14
|
+
if params_clone[:created_at_lt].present?
|
15
|
+
params_clone[:created_at_lt] = Time.zone.parse(params_clone[:created_at_lt]).end_of_day rescue ''
|
16
|
+
end
|
17
|
+
|
18
|
+
params_clone[:send_emails_count] = Integer(params_clone[:send_emails_count]) rescue ''
|
19
|
+
|
20
|
+
@search = Spree::AbandonedOrder.ransack(params_clone)
|
21
|
+
@abandoned_carts = @search.result(distinct: true).page(params[:page]).per(Spree::Config[:admin_products_per_page])
|
22
|
+
end
|
23
|
+
|
24
|
+
def destroy
|
25
|
+
@abandoned_cart.destroy
|
26
|
+
|
27
|
+
redirect_to admin_abandoned_orders_path
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def find_abandoned_cart
|
33
|
+
@abandoned_cart = Spree::AbandonedOrder.find(params[:id])
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Spree
|
2
|
+
class AbandonedCartMailer < ActionMailer::Base
|
3
|
+
default from: Spree::AbandonedCartEmailConfig::Config.email_from
|
4
|
+
|
5
|
+
def abandoned_email(order)
|
6
|
+
@order = order
|
7
|
+
mail to: @order.email, subject: Spree.t('abandoned_cart.email.subject')
|
8
|
+
end
|
9
|
+
|
10
|
+
def abandoned_second_email(order)
|
11
|
+
@order = order
|
12
|
+
mail to: @order.email, subject: Spree.t('abandoned_cart.email.subject_second')
|
13
|
+
end
|
14
|
+
|
15
|
+
def abandoned_third_email(order)
|
16
|
+
@order = order
|
17
|
+
mail to: @order.email, subject: Spree.t('abandoned_cart.email.subject_third')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
Spree::Order.class_eval do
|
2
|
+
has_one :abandoned_order, class_name: 'Spree::AbandonedOrder', foreign_key: :spree_order_id, dependent: :destroy
|
3
|
+
|
4
|
+
def self.email_eligible_abandoned_email_orders
|
5
|
+
eligible_abandoned_email_orders.each {|o| o.send_abandoned_email }
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.eligible_abandoned_email_orders
|
9
|
+
where('state != ?
|
10
|
+
AND (payment_state IS NULL OR payment_state != ?)
|
11
|
+
AND email is NOT NULL
|
12
|
+
AND abandoned_email_sent_at IS NULL
|
13
|
+
AND (spree_orders.created_at BETWEEN ? AND ? )',
|
14
|
+
'complete',
|
15
|
+
'paid',
|
16
|
+
(Time.zone.now - Spree::AbandonedCartEmailConfig::Config.email_timeframe_from),
|
17
|
+
(Time.zone.now - Spree::AbandonedCartEmailConfig::Config.email_timeframe_to)).joins(:line_items).distinct
|
18
|
+
end
|
19
|
+
|
20
|
+
def send_abandoned_email
|
21
|
+
# Don't send anything if the order has no line items.
|
22
|
+
return if line_items.empty?
|
23
|
+
|
24
|
+
Spree::AbandonedCartMailer.abandoned_email(self).deliver_now
|
25
|
+
mark_abandoned_email_as_sent
|
26
|
+
log_abandoned_cart
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def mark_abandoned_email_as_sent
|
32
|
+
update_column :abandoned_email_sent_at, Time.zone.now
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_abandoned_cart
|
36
|
+
Spree::AbandonedOrder.create spree_order_id: id
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
Spree::OrderUpdater.class_eval do
|
2
|
+
|
3
|
+
def update_payment_state
|
4
|
+
last_state = order.payment_state
|
5
|
+
|
6
|
+
if payments.present? && payments.valid.size == 0
|
7
|
+
order.payment_state = 'failed'
|
8
|
+
elsif order.state == 'canceled' && order.payment_total == 0
|
9
|
+
order.payment_state = 'void'
|
10
|
+
else
|
11
|
+
order.payment_state = 'balance_due' if order.outstanding_balance > 0
|
12
|
+
order.payment_state = 'credit_owed' if order.outstanding_balance < 0
|
13
|
+
|
14
|
+
if !order.outstanding_balance?
|
15
|
+
order.payment_state = 'paid'
|
16
|
+
# Update abandoned order after changing order status to 'paid'
|
17
|
+
if order.abandoned_order.present?
|
18
|
+
order.abandoned_order.update(order_accomplished: true, accomplished_at: Time.zone.now)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
order.state_changed('payment') if last_state != order.payment_state
|
23
|
+
order.payment_state
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class Spree::AbandonedOrder < ActiveRecord::Base
|
2
|
+
belongs_to :order, class_name: 'Spree::Order', foreign_key: :spree_order_id
|
3
|
+
|
4
|
+
after_create :increment_send_emails_count
|
5
|
+
|
6
|
+
def self.email_eligible_abandoned_email_orders(email_number=1)
|
7
|
+
eligible_orders_next_attempt(email_number).each { |abandoned_order| abandoned_order.process_abandoned_email(email_number) }
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.eligible_orders_next_attempt(emails_count = 1, hours_period = [25, 23])
|
11
|
+
hours_period = [3, 1] unless ENV['RAILS_ENV'] == 'production'
|
12
|
+
|
13
|
+
where(send_emails_count: emails_count,
|
14
|
+
order_accomplished: false,
|
15
|
+
updated_at: (Time.zone.now - hours_period[0].hours)..(Time.zone.now - hours_period[1].hours))
|
16
|
+
end
|
17
|
+
|
18
|
+
def process_abandoned_email(email_number=1)
|
19
|
+
(delete && return) unless self.order.email.present?
|
20
|
+
|
21
|
+
Spree::AbandonedCartMailer.send("abandoned_#{email_number == 1 ? 'second' : 'third'}_email", self.order).deliver_now
|
22
|
+
increment_send_emails_count
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def increment_send_emails_count
|
28
|
+
increment!(:send_emails_count)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
Deface::Override.new(
|
2
|
+
virtual_path: 'spree/admin/orders/index',
|
3
|
+
name: 'admin_abandoned_index_button',
|
4
|
+
insert_before: "erb[loud]:contains('Spree.t(:new_order)')",
|
5
|
+
text: "<%= button_link_to Spree.t(:abandoned_index),
|
6
|
+
admin_abandoned_orders_path, { style: 'margin-right:5px;',
|
7
|
+
class: 'btn-primary', icon: 'ok', id: 'admin_abandoned_index' }%>"
|
8
|
+
)
|
@@ -0,0 +1,145 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
5
|
+
<title><%= Spree.t("abandoned_cart.email.subject") %></title>
|
6
|
+
<style type="text/css">
|
7
|
+
#outlook a{padding:0;} /* Force Outlook to provide a "view in browser" button. */
|
8
|
+
body{width:100% !important;} .ReadMsgBody{width:100%;} .ExternalClass{width:100%;} /* Force Hotmail to display emails at full width */
|
9
|
+
body{-webkit-text-size-adjust:none;} /* Prevent Webkit platforms from changing default text sizes. */
|
10
|
+
|
11
|
+
body{font-family:Arial;margin:0; padding:0;}
|
12
|
+
img{border:0; height:auto; line-height:100%; outline:none; text-decoration:none;}
|
13
|
+
table td{border-collapse:collapse;}
|
14
|
+
#backgroundTable{height:100% !important; margin:0; padding:0; width:100% !important;}
|
15
|
+
|
16
|
+
body, #backgroundTable{
|
17
|
+
background-color:#FAFAFA;
|
18
|
+
}
|
19
|
+
|
20
|
+
#templateContainer{
|
21
|
+
border: 1px solid #DDDDDD;
|
22
|
+
margin-top: 10px;
|
23
|
+
padding: 20px;
|
24
|
+
}
|
25
|
+
|
26
|
+
h1, .h1{
|
27
|
+
color:#202020;
|
28
|
+
display:block;
|
29
|
+
font-family:Arial;
|
30
|
+
font-size:34px;
|
31
|
+
font-weight:bold;
|
32
|
+
line-height:100%;
|
33
|
+
margin-top:0;
|
34
|
+
margin-right:0;
|
35
|
+
margin-bottom:10px;
|
36
|
+
margin-left:0;
|
37
|
+
text-align:left;
|
38
|
+
}
|
39
|
+
|
40
|
+
h2, .h2{
|
41
|
+
color:#202020;
|
42
|
+
display:block;
|
43
|
+
font-family:Arial;
|
44
|
+
font-size:30px;
|
45
|
+
font-weight:bold;
|
46
|
+
line-height:100%;
|
47
|
+
margin-top:0;
|
48
|
+
margin-right:0;
|
49
|
+
margin-bottom:10px;
|
50
|
+
margin-left:0;
|
51
|
+
text-align:left;
|
52
|
+
}
|
53
|
+
|
54
|
+
h3, .h3{
|
55
|
+
color:#202020;
|
56
|
+
display:block;
|
57
|
+
font-family:Arial;
|
58
|
+
font-size:26px;
|
59
|
+
font-weight:bold;
|
60
|
+
line-height:100%;
|
61
|
+
margin-top:0;
|
62
|
+
margin-right:0;
|
63
|
+
margin-bottom:10px;
|
64
|
+
margin-left:0;
|
65
|
+
text-align:left;
|
66
|
+
}
|
67
|
+
|
68
|
+
h4, .h4{
|
69
|
+
color:#202020;
|
70
|
+
display:block;
|
71
|
+
font-family:Arial;
|
72
|
+
font-size:22px;
|
73
|
+
font-weight:bold;
|
74
|
+
line-height:100%;
|
75
|
+
margin-top:0;
|
76
|
+
margin-right:0;
|
77
|
+
margin-bottom:10px;
|
78
|
+
margin-left:0;
|
79
|
+
text-align:left;
|
80
|
+
}
|
81
|
+
|
82
|
+
#templateContainer, .bodyContent{
|
83
|
+
background-color:#FFFFFF;
|
84
|
+
}
|
85
|
+
|
86
|
+
.bodyContent div{
|
87
|
+
color:#505050;
|
88
|
+
font-family:Arial;
|
89
|
+
font-size:14px;
|
90
|
+
line-height:150%;
|
91
|
+
text-align:left;
|
92
|
+
}
|
93
|
+
|
94
|
+
.bodyContent div a:link, .bodyContent div a:visited, /* Yahoo! Mail Override */ .bodyContent div a .yshortcuts /* Yahoo! Mail Override */{
|
95
|
+
color:#336699;
|
96
|
+
font-weight:normal;
|
97
|
+
text-decoration:underline;
|
98
|
+
}
|
99
|
+
|
100
|
+
.bodyContent img{
|
101
|
+
display:inline;
|
102
|
+
height:auto;
|
103
|
+
}
|
104
|
+
|
105
|
+
#items {
|
106
|
+
margin: 10px 0;
|
107
|
+
}
|
108
|
+
</style>
|
109
|
+
</head>
|
110
|
+
<body leftmargin="0" marginwidth="0" topmargin="0" marginheight="0" offset="0">
|
111
|
+
<center>
|
112
|
+
<table border="0" cellpadding="0" cellspacing="0" height="100%" width="100%" id="backgroundTable">
|
113
|
+
<tr>
|
114
|
+
<td align="center" valign="top">
|
115
|
+
<table border="0" cellpadding="0" cellspacing="0" width="600" id="templateContainer">
|
116
|
+
<tr>
|
117
|
+
<td align="center" valign="top">
|
118
|
+
<table border="0" cellpadding="0" cellspacing="0" width="600" id="templateBody">
|
119
|
+
<tr>
|
120
|
+
<td valign="top" class="bodyContent">
|
121
|
+
We noticed you still have some things in your shopping cart — if you need help with sizes or have questions, please call, email, or chat and we'll do our best.
|
122
|
+
|
123
|
+
<table cellpadding="0" cellspacing="0" border="0" align="center" id="items">
|
124
|
+
<% @order.line_items.each do |line_item| %>
|
125
|
+
<% variant = line_item.variant %>
|
126
|
+
<tr>
|
127
|
+
<td class="cart-item-description" data-hook="cart_item_description">
|
128
|
+
<strong><%= link_to variant.product.name, spree.product_url(variant.product) %></strong>
|
129
|
+
</td>
|
130
|
+
</tr>
|
131
|
+
<% end %>
|
132
|
+
</table>
|
133
|
+
</td>
|
134
|
+
</tr>
|
135
|
+
</table>
|
136
|
+
</td>
|
137
|
+
</tr>
|
138
|
+
</table>
|
139
|
+
<br />
|
140
|
+
</td>
|
141
|
+
</tr>
|
142
|
+
</table>
|
143
|
+
</center>
|
144
|
+
</body>
|
145
|
+
</html>
|