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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +17 -0
  3. data/.gitlab-ci.yml +15 -0
  4. data/.rspec +1 -0
  5. data/Gemfile +8 -0
  6. data/LICENSE +26 -0
  7. data/README.md +69 -0
  8. data/Rakefile +15 -0
  9. data/app/controllers/spree/admin/abandoned_orders_controller.rb +35 -0
  10. data/app/mailers/spree/abandoned_cart_mailer.rb +20 -0
  11. data/app/models/order_decorator.rb +38 -0
  12. data/app/models/order_updater_decorator.rb +25 -0
  13. data/app/models/spree/abandoned_cart_email_configuration.rb +5 -0
  14. data/app/models/spree/abandoned_order.rb +30 -0
  15. data/app/overrides/add_button_to_orders_tab_for_abandoned_index_page.rb +8 -0
  16. data/app/views/spree/abandoned_cart_mailer/abandoned_email.html.erb +145 -0
  17. data/app/views/spree/abandoned_cart_mailer/abandoned_second_email.html.erb +145 -0
  18. data/app/views/spree/abandoned_cart_mailer/abandoned_third_email.html.erb +145 -0
  19. data/app/views/spree/admin/abandoned_orders/_filters_bar.html.haml +49 -0
  20. data/app/views/spree/admin/abandoned_orders/_list.html.haml +22 -0
  21. data/app/views/spree/admin/abandoned_orders/_list_respond.js.erb +1 -0
  22. data/app/views/spree/admin/abandoned_orders/_order_summary.html.haml +16 -0
  23. data/app/views/spree/admin/abandoned_orders/_titles.html.haml +6 -0
  24. data/app/views/spree/admin/abandoned_orders/index.html.haml +3 -0
  25. data/app/views/spree/admin/abandoned_orders/index.js.haml +1 -0
  26. data/app/views/spree/admin/abandoned_orders/show.html.haml +2 -0
  27. data/config/initializers/load_preferences.rb +5 -0
  28. data/config/locales/en.yml +17 -0
  29. data/config/locales/ru.yml +17 -0
  30. data/config/routes.rb +9 -0
  31. data/db/migrate/20130722151112_add_abandoned_email_sent_at_to_spree_orders.rb +5 -0
  32. data/db/migrate/20150825122125_create_spree_abandoned_orders.rb +12 -0
  33. data/lib/generators/spree_abandoned_cart/install/install_generator.rb +21 -0
  34. data/lib/spree_abandoned_cart.rb +3 -0
  35. data/lib/spree_abandoned_cart/engine.rb +22 -0
  36. data/lib/spree_abandoned_cart/factories.rb +6 -0
  37. data/script/rails +7 -0
  38. data/spec/models/order_spec.rb +35 -0
  39. data/spec/spec_helper.rb +86 -0
  40. data/spree_abandoned_cart.gemspec +38 -0
  41. metadata +68 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c08326ca4ade3d5d8e45e845d5f07e1c73c73cdb
4
- data.tar.gz: e090c04447dddae062626ce14ae815333a6c17ac
3
+ metadata.gz: 2f4079aa9bcd323157d4a9066b95c195d3f7ad11
4
+ data.tar.gz: 5cdbf279a566c2a57829539d77a2c02f02618c9b
5
5
  SHA512:
6
- metadata.gz: cc133361977365c03ac99ce87755aced4ecf9f8d11521850c1feaa3598af0383ae370dae888b687348ca1ac4ff38d4436e476fc75d3068becdfeb03f71e63cf5
7
- data.tar.gz: 6c6911eaf1d4975a8233bed796a241803d7966ff21d8091c15f7bb810e944f24c97da732ffae0d67911fa3a4504147ba32a6296e01e82fad01538b64360f81c3
6
+ metadata.gz: 8d6cd065fa913f3e49bfd3ef90258606d2d004ca574d0bc43872a2e5e288f712bd5e3f0be7e99f38b9d570762febc1135afabf5a65662fdf4673a02f21211721
7
+ data.tar.gz: 8863266e96146919e7987b2295400c2e0854ce20d937e81cb3126b1102b99171753c1fb107014f7bd2a9d0d980ec653c0038c6ed8887f907c4ffbe4305620934
@@ -0,0 +1,17 @@
1
+ \#*
2
+ *~
3
+ .#*
4
+ .DS_Store
5
+ .idea
6
+ .project
7
+ .sass-cache
8
+ .ruby-version
9
+ .ruby-gemset
10
+ coverage
11
+ Gemfile.lock
12
+ tmp
13
+ nbproject
14
+ pkg
15
+ *.swp
16
+ spec/dummy
17
+ /.bundle
@@ -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
@@ -0,0 +1,8 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'spree', '~> 3.6'
4
+ gem 'spree_auth_devise', '~> 3.3'
5
+ gem 'haml'
6
+ gem 'haml-rails'
7
+
8
+ gemspec
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.
@@ -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
+ ```
@@ -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,5 @@
1
+ class Spree::AbandonedCartEmailConfiguration < Spree::Preferences::Configuration
2
+ preference :email_timeframe_from, :integer
3
+ preference :email_timeframe_to, :integer
4
+ preference :email_from, :string
5
+ 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>