spree_abandoned_cart 0.3.0 → 0.3.3

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 (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>