DigitalPianism_Abandonedcarts - Version 1.0.0

Version Notes

- Full refactor of the module
- Add two grids to the backend to see the abandoned carts
- Add a log database table to easily see what's going on
- Implement an autologin link system
- Implement Google Campaign tags
- Improve the templates to list all items
- Change the way dryrun and test email behaves
- Add notification flags columns to the native abandoned carts report

Download this release

Release Info

Developer Digital Pianism
Extension DigitalPianism_Abandonedcarts
Version 1.0.0
Comparing to
See all releases


Code changes from version 0.3.6 to 1.0.0

Files changed (38) hide show
  1. app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Abandonedcarts.php +49 -0
  2. app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Abandonedcarts/Grid.php +210 -0
  3. app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Logs.php +20 -0
  4. app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Logs/Grid.php +76 -0
  5. app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Saleabandonedcarts.php +49 -0
  6. app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Saleabandonedcarts/Grid.php +218 -0
  7. app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/System/Config/Form/Button.php +0 -54
  8. app/code/community/DigitalPianism/Abandonedcarts/Helper/Data.php +58 -26
  9. app/code/community/DigitalPianism/Abandonedcarts/Model/Adminhtml/Observer.php +87 -0
  10. app/code/community/DigitalPianism/Abandonedcarts/Model/Collection.php +280 -0
  11. app/code/community/DigitalPianism/Abandonedcarts/Model/Link.php +14 -0
  12. app/code/community/DigitalPianism/Abandonedcarts/Model/Link/Cleaner.php +31 -0
  13. app/code/community/DigitalPianism/Abandonedcarts/Model/Log.php +25 -0
  14. app/code/community/DigitalPianism/Abandonedcarts/Model/Notifier.php +637 -0
  15. app/code/community/DigitalPianism/Abandonedcarts/Model/Observer.php +0 -742
  16. app/code/community/DigitalPianism/Abandonedcarts/Model/Resource/Link.php +14 -0
  17. app/code/community/DigitalPianism/Abandonedcarts/Model/Resource/Link/Collection.php +14 -0
  18. app/code/community/DigitalPianism/Abandonedcarts/Model/Resource/Log.php +14 -0
  19. app/code/community/DigitalPianism/Abandonedcarts/Model/Resource/Log/Collection.php +14 -0
  20. app/code/community/DigitalPianism/Abandonedcarts/controllers/Adminhtml/AbandonedcartsController.php +113 -11
  21. app/code/community/DigitalPianism/Abandonedcarts/controllers/IndexController.php +50 -0
  22. app/code/community/DigitalPianism/Abandonedcarts/data/abandonedcarts_setup/data-upgrade-1.0.0-1.0.1.php +33 -0
  23. app/code/community/DigitalPianism/Abandonedcarts/etc/adminhtml.xml +60 -0
  24. app/code/community/DigitalPianism/Abandonedcarts/etc/config.xml +94 -5
  25. app/code/community/DigitalPianism/Abandonedcarts/etc/system.xml +106 -43
  26. app/code/community/DigitalPianism/Abandonedcarts/sql/abandonedcarts_setup/upgrade-0.3.6-1.0.0.php +27 -0
  27. app/design/adminhtml/default/default/layout/digitalpianism/abandonedcarts.xml +10 -0
  28. app/design/adminhtml/default/default/template/digitalpianism/abandonedcarts/list.phtml +12 -0
  29. app/design/adminhtml/default/default/template/digitalpianism/abandonedcarts/system/config/button.phtml +0 -17
  30. app/design/frontend/base/default/template/digitalpianism/abandonedcarts/email/items.phtml +26 -0
  31. app/design/frontend/base/default/template/digitalpianism/abandonedcarts/email/sale_items.phtml +40 -0
  32. app/locale/en_US/DigitalPianism_Abandonedcarts.csv +32 -4
  33. app/locale/en_US/template/email/digitalpianism/abandonedcarts/sales_abandonedcarts.html +3 -5
  34. app/locale/en_US/template/email/digitalpianism/abandonedcarts/sales_abandonedcarts_sale.html +4 -10
  35. app/locale/fr_FR/DigitalPianism_Abandonedcarts.csv +33 -5
  36. app/locale/fr_FR/template/email/digitalpianism/abandonedcarts/sales_abandonedcarts.html +4 -6
  37. app/locale/fr_FR/template/email/digitalpianism/abandonedcarts/sales_abandonedcarts_sale.html +5 -12
  38. package.xml +12 -5
app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Abandonedcarts.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Block_Adminhtml_Abandonedcarts
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Block_Adminhtml_Abandonedcarts extends Mage_Adminhtml_Block_Widget_Grid_Container
7
+ {
8
+ /**
9
+ * Constructor
10
+ */
11
+ public function __construct()
12
+ {
13
+ $this->_controller = 'adminhtml_abandonedcarts';
14
+ $this->_blockGroup = 'abandonedcarts';
15
+ $this->_headerText = Mage::helper('abandonedcarts')->__('Abandoned Carts (Applied delay: %s days)', Mage::getStoreConfig('abandonedcartsconfig/options/notify_delay'));
16
+ parent::__construct();
17
+ $this->_removeButton('add');
18
+ $this->_addButton('notify', array(
19
+ 'label' => Mage::helper('abandonedcarts')->__('Send notifications'),
20
+ 'onclick' => "setLocation('".$this->getUrl('*/*/notifyAll')."')",
21
+ ));
22
+ $this->setTemplate('digitalpianism/abandonedcarts/list.phtml');
23
+ }
24
+
25
+ /**
26
+ * Prepare the layout
27
+ */
28
+ protected function _prepareLayout()
29
+ {
30
+ // Display store switcher if system has more one store
31
+ if (!Mage::app()->isSingleStoreMode())
32
+ {
33
+ $this->setChild('store_switcher', $this->getLayout()->createBlock('adminhtml/store_switcher')
34
+ ->setUseConfirm(false)
35
+ ->setSwitchUrl($this->getUrl('*/*/*', array('store' => null)))
36
+ );
37
+ }
38
+ return parent::_prepareLayout();
39
+ }
40
+
41
+ /**
42
+ * Getter for the store switcher HTML
43
+ */
44
+ public function getStoreSwitcherHtml()
45
+ {
46
+ return $this->getChildHtml('store_switcher');
47
+ }
48
+
49
+ }
app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Abandonedcarts/Grid.php ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Block_Adminhtml_Abandonedcarts_Grid
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Block_Adminhtml_Abandonedcarts_Grid extends Mage_Adminhtml_Block_Widget_Grid
7
+ {
8
+
9
+ /**
10
+ *
11
+ */
12
+ public function __construct()
13
+ {
14
+ parent::__construct();
15
+ $this->setId('abandonedcartsGrid');
16
+ $this->setDefaultSort('cart_updated_at');
17
+ $this->setDefaultDir('DESC');
18
+ $this->setSaveParametersInSession(true);
19
+ $this->setUseAjax(true);
20
+ }
21
+
22
+ /**
23
+ * @return mixed
24
+ */
25
+ protected function _getStore()
26
+ {
27
+ $storeId = (int) $this->getRequest()->getParam('store', 0);
28
+ return Mage::app()->getStore($storeId);
29
+ }
30
+
31
+ protected function _prepareCollection()
32
+ {
33
+ // Delay
34
+ $delay = Mage::getStoreConfig('abandonedcartsconfig/options/notify_delay');
35
+ $delay = date('Y-m-d H:i:s', time() - $delay * 24 * 3600);
36
+
37
+ // Default store and website
38
+ $defaults = $this->_getDefaultStoreAndWebsite();
39
+
40
+ // Store and website from the multistore switcher
41
+ $store = $this->_getStore();
42
+ if ($storeId = $store->getId())
43
+ {
44
+ $defaults = array(
45
+ $storeId,
46
+ Mage::getModel('core/store')->load($storeId)->getWebsiteId()
47
+ );
48
+ }
49
+
50
+ $collection = Mage::getModel('abandonedcarts/collection')->getCollection($delay, $defaults[0], $defaults[1]);
51
+
52
+ // Group by to have a nice grid
53
+ if (Mage::helper('catalog/product_flat')->isEnabled()) {
54
+ $collection->getSelect()->columns(
55
+ array(
56
+ 'product_ids' => 'GROUP_CONCAT(e.entity_id)',
57
+ 'product_names' => 'GROUP_CONCAT(catalog_flat.name)',
58
+ 'product_prices' => 'SUM(catalog_flat.price)'
59
+ )
60
+ );
61
+ } else {
62
+ $collection->getSelect()->columns(
63
+ array(
64
+ 'product_ids' => 'GROUP_CONCAT(e.entity_id)',
65
+ 'product_names' => 'GROUP_CONCAT(catalog_name.value)',
66
+ 'product_prices' => 'SUM(catalog_price.value)'
67
+ )
68
+ );
69
+ }
70
+ $collection->getSelect()->group('customer_email');
71
+
72
+ $this->setCollection($collection);
73
+ return parent::_prepareCollection();
74
+ }
75
+
76
+ protected function _prepareColumns()
77
+ {
78
+ $this->addColumn('customer_email', array(
79
+ 'header' => Mage::helper('abandonedcarts')->__('Customer Email'),
80
+ 'index' => 'customer_email',
81
+ 'filter_condition_callback' => array($this, 'filterCallback')
82
+ ));
83
+
84
+ $this->addColumn('customer_firstname', array(
85
+ 'header' => Mage::helper('abandonedcarts')->__('Customer Firstname'),
86
+ 'index' => 'customer_firstname',
87
+ 'filter_condition_callback' => array($this, 'filterCallback')
88
+ ));
89
+
90
+ $this->addColumn('customer_lastname', array(
91
+ 'header' => Mage::helper('abandonedcarts')->__('Customer Lastname'),
92
+ 'index' => 'customer_lastname',
93
+ 'filter_condition_callback' => array($this, 'filterCallback')
94
+ ));
95
+
96
+ $this->addColumn('product_ids', array(
97
+ 'header' => Mage::helper('abandonedcarts')->__('Product Ids'),
98
+ 'index' => 'product_ids',
99
+ 'filter_index' => "e.entity_id",
100
+ 'filter_condition_callback' => array($this, 'filterEqualCallback')
101
+ ));
102
+
103
+ $this->addColumn('product_names', array(
104
+ 'header' => Mage::helper('abandonedcarts')->__('Product Names'),
105
+ 'index' => 'product_names',
106
+ 'filter_index' => (Mage::helper('catalog/product_flat')->isEnabled() ? "catalog_flat.name" : "catalog_name.value"),
107
+ 'filter_condition_callback' => array($this, 'filterEqualCallback')
108
+ ));
109
+
110
+ $this->addColumn('product_prices', array(
111
+ 'header' => Mage::helper('abandonedcarts')->__('Cart Total'),
112
+ 'index' => 'product_prices',
113
+ 'filter' => false
114
+ ));
115
+
116
+ // Output format for the start and end dates
117
+ $outputFormat = Mage::app()->getLocale()->getDateTimeFormat(Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM);
118
+
119
+ $this->addColumn('cart_updated_at', array(
120
+ 'header' => Mage::helper('abandonedcarts')->__('Cart Updated At'),
121
+ 'index' => 'cart_updated_at',
122
+ 'type' => 'datetime',
123
+ 'format' => $outputFormat,
124
+ 'default' => ' -- ',
125
+ 'filter_index' => 'quote_table.updated_at',
126
+ 'filter_condition_callback' => array($this, 'filterDateCallback')
127
+ ));
128
+
129
+ return parent::_prepareColumns();
130
+ }
131
+
132
+ /**
133
+ * @return $this
134
+ */
135
+ protected function _prepareMassaction()
136
+ {
137
+ $this->setMassactionIdField('customer_email');
138
+ $this->getMassactionBlock()->setFormFieldName('abandonedcarts');
139
+
140
+ $this->getMassactionBlock()->addItem('notify', array(
141
+ 'label' => Mage::helper('abandonedcarts')->__('Send notification'),
142
+ 'url' => $this->getUrl('*/*/notify')
143
+ ));
144
+
145
+ return $this;
146
+ }
147
+
148
+ /**
149
+ * @return array
150
+ */
151
+ protected function _getDefaultStoreAndWebsite()
152
+ {
153
+ foreach (Mage::app()->getWebsites() as $website) {
154
+ // Get the website id
155
+ $websiteId = $website->getWebsiteId();
156
+ foreach ($website->getGroups() as $group) {
157
+ $stores = $group->getStores();
158
+ foreach ($stores as $store) {
159
+
160
+ // Get the store id
161
+ $storeId = $store->getStoreId();
162
+ break 3;
163
+ }
164
+ }
165
+ }
166
+ return array($storeId, $websiteId);
167
+ }
168
+
169
+ /**
170
+ * @return string
171
+ */
172
+ public function getGridUrl()
173
+ {
174
+ return $this->getUrl('*/*/grid', array('current' => true));
175
+ }
176
+
177
+ /**
178
+ * @param $collection
179
+ * @param $column
180
+ */
181
+ public function filterCallback($collection, $column)
182
+ {
183
+ $field = $column->getFilterIndex() ? $column->getFilterIndex() : $column->getIndex();
184
+ $value = $column->getFilter()->getValue();
185
+ $collection->getSelect()->where("$field like ?", '%' . $value . '%');
186
+ }
187
+
188
+ /**
189
+ * @param $collection
190
+ * @param $column
191
+ */
192
+ public function filterEqualCallback($collection, $column)
193
+ {
194
+ $field = $column->getFilterIndex() ? $column->getFilterIndex() : $column->getIndex();
195
+ $value = $column->getFilter()->getValue();
196
+ $collection->getSelect()->where("$field = ?", $value);
197
+ }
198
+
199
+ /**
200
+ * @param $collection
201
+ * @param $column
202
+ */
203
+ public function filterDateCallback($collection, $column)
204
+ {
205
+ $field = $column->getFilterIndex() ? $column->getFilterIndex() : $column->getIndex();
206
+ $value = $column->getFilter()->getValue();
207
+ $collection->getSelect()->where("$field > '" . $value['from']->toString('Y-MM-dd HH:mm:ss') . "' AND $field < '" . $value['to']->toString('Y-MM-dd HH:mm:ss') . "'");
208
+ }
209
+
210
+ }
app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Logs.php ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Block_Adminhtml_Logs
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Block_Adminhtml_Logs extends Mage_Adminhtml_Block_Widget_Grid_Container
7
+ {
8
+ /**
9
+ * Constructor
10
+ */
11
+ public function __construct()
12
+ {
13
+ $this->_controller = 'adminhtml_logs';
14
+ $this->_blockGroup = 'abandonedcarts';
15
+ $this->_headerText = Mage::helper('abandonedcarts')->__('Abandoned Carts Logs');
16
+ parent::__construct();
17
+ $this->_removeButton('add');
18
+ }
19
+
20
+ }
app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Logs/Grid.php ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Block_Adminhtml_Logs_Grid
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Block_Adminhtml_Logs_Grid extends Mage_Adminhtml_Block_Widget_Grid
7
+ {
8
+
9
+ /**
10
+ *
11
+ */
12
+ public function __construct()
13
+ {
14
+ parent::__construct();
15
+ $this->setId('abandonedcartLogsGrid');
16
+ $this->setDefaultSort('log_id');
17
+ $this->setDefaultDir('DESC');
18
+ $this->setSaveParametersInSession(true);
19
+ }
20
+
21
+ protected function _prepareCollection()
22
+ {
23
+ $collection = Mage::getResourceModel('abandonedcarts/log_collection');
24
+ $this->setCollection($collection);
25
+ return parent::_prepareCollection();
26
+ }
27
+
28
+ protected function _prepareColumns()
29
+ {
30
+ $this->addColumn('customer_email', array(
31
+ 'header' => Mage::helper('abandonedcarts')->__('Customer Email'),
32
+ 'index' => 'customer_email',
33
+ ));
34
+
35
+ $this->addColumn('type', array(
36
+ 'header' => Mage::helper('abandonedcarts')->__('Type'),
37
+ 'index' => 'type',
38
+ 'type' => 'options',
39
+ 'options' => Mage::getModel('abandonedcarts/log')->toOptionArray()
40
+ ));
41
+
42
+ $this->addColumn('comment', array(
43
+ 'header' => Mage::helper('abandonedcarts')->__('Comment'),
44
+ 'index' => 'comment',
45
+ ));
46
+
47
+ $this->addColumn('store', array(
48
+ 'header' => Mage::helper('abandonedcarts')->__('Store #'),
49
+ 'index' => 'store',
50
+ ));
51
+
52
+ $this->addColumn('dryrun', array(
53
+ 'header' => Mage::helper('abandonedcarts')->__('Dry Run'),
54
+ 'type' => 'options',
55
+ 'index' => 'dryrun',
56
+ 'options' => array(
57
+ 0 => Mage::helper('abandonedcarts')->__("No"),
58
+ 1 => Mage::helper('abandonedcarts')->__("Yes")
59
+ )
60
+ ));
61
+
62
+ // Output format for the start and end dates
63
+ $outputFormat = Mage::app()->getLocale()->getDateTimeFormat(Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM);
64
+
65
+ $this->addColumn('added', array(
66
+ 'header' => Mage::helper('abandonedcarts')->__('Date'),
67
+ 'index' => 'added',
68
+ 'type' => 'datetime',
69
+ 'format' => $outputFormat,
70
+ 'default' => ' -- '
71
+ ));
72
+
73
+ return parent::_prepareColumns();
74
+ }
75
+
76
+ }
app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Saleabandonedcarts.php ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Block_Adminhtml_Saleabandonedcarts
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Block_Adminhtml_Saleabandonedcarts extends Mage_Adminhtml_Block_Widget_Grid_Container
7
+ {
8
+ /**
9
+ * Constructor
10
+ */
11
+ public function __construct()
12
+ {
13
+ $this->_controller = 'adminhtml_saleabandonedcarts';
14
+ $this->_blockGroup = 'abandonedcarts';
15
+ $this->_headerText = Mage::helper('abandonedcarts')->__('Sale Abandoned Carts');
16
+ parent::__construct();
17
+ $this->_removeButton('add');
18
+ $this->_addButton('notify', array(
19
+ 'label' => Mage::helper('abandonedcarts')->__('Send notifications'),
20
+ 'onclick' => "setLocation('".$this->getUrl('*/*/notifySaleAll')."')",
21
+ ));
22
+ $this->setTemplate('digitalpianism/abandonedcarts/list.phtml');
23
+ }
24
+
25
+ /**
26
+ * Prepare the layout
27
+ */
28
+ protected function _prepareLayout()
29
+ {
30
+ // Display store switcher if system has more one store
31
+ if (!Mage::app()->isSingleStoreMode())
32
+ {
33
+ $this->setChild('store_switcher', $this->getLayout()->createBlock('adminhtml/store_switcher')
34
+ ->setUseConfirm(false)
35
+ ->setSwitchUrl($this->getUrl('*/*/*', array('store' => null)))
36
+ );
37
+ }
38
+ return parent::_prepareLayout();
39
+ }
40
+
41
+ /**
42
+ * Getter for the store switcher HTML
43
+ */
44
+ public function getStoreSwitcherHtml()
45
+ {
46
+ return $this->getChildHtml('store_switcher');
47
+ }
48
+
49
+ }
app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/Saleabandonedcarts/Grid.php ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Block_Adminhtml_Saleabandonedcarts_Grid
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Block_Adminhtml_Saleabandonedcarts_Grid extends Mage_Adminhtml_Block_Widget_Grid
7
+ {
8
+
9
+ /**
10
+ *
11
+ */
12
+ public function __construct()
13
+ {
14
+ parent::__construct();
15
+ $this->setId('saleabandonedcartsGrid');
16
+ $this->setDefaultSort('cart_updated_at');
17
+ $this->setDefaultDir('DESC');
18
+ $this->setSaveParametersInSession(true);
19
+ $this->setUseAjax(true);
20
+ }
21
+
22
+ /**
23
+ * @return mixed
24
+ */
25
+ protected function _getStore()
26
+ {
27
+ $storeId = (int) $this->getRequest()->getParam('store', 0);
28
+ return Mage::app()->getStore($storeId);
29
+ }
30
+
31
+ protected function _prepareCollection()
32
+ {
33
+ // Default store and website
34
+ $defaults = $this->_getDefaultStoreAndWebsite();
35
+
36
+ // Store and website from the multistore switcher
37
+ $store = $this->_getStore();
38
+ if ($storeId = $store->getId())
39
+ {
40
+ $defaults = array(
41
+ $storeId,
42
+ Mage::getModel('core/store')->load($storeId)->getWebsiteId()
43
+ );
44
+ }
45
+
46
+ $collection = Mage::getModel('abandonedcarts/collection')->getSalesCollection($defaults[0], $defaults[1]);
47
+
48
+ // Group by to have a nice grid
49
+ $collection->getSelect()->group('customer_email');
50
+
51
+ if (Mage::helper('catalog/product_flat')->isEnabled()) {
52
+ $collection->getSelect()->columns(
53
+ array(
54
+ 'product_ids' => 'GROUP_CONCAT(e.entity_id)',
55
+ 'product_names' => 'GROUP_CONCAT(catalog_flat.name)',
56
+ 'product_prices' => 'SUM(quote_items.price)',
57
+ 'product_special_prices' => 'SUM(IFNULL(catalog_flat.special_price,quote_items.price))',
58
+ )
59
+ );
60
+
61
+ $collection->getSelect()->having("SUM(quote_items.price) < SUM(IFNULL(catalog_flat.special_price,quote_items.price))");
62
+ } else {
63
+ $collection->getSelect()->columns(
64
+ array(
65
+ 'product_ids' => 'GROUP_CONCAT(e.entity_id)',
66
+ 'product_names' => 'GROUP_CONCAT(catalog_name.value)',
67
+ 'product_prices' => 'SUM(quote_items.price)',
68
+ 'product_special_prices' => 'SUM(IFNULL(catalog_sprice.value,quote_items.price))',
69
+ )
70
+ );
71
+
72
+ $collection->getSelect()->having("SUM(quote_items.price) > SUM(IFNULL(catalog_sprice.value,quote_items.price))");
73
+ }
74
+
75
+ $this->setCollection($collection);
76
+ return parent::_prepareCollection();
77
+ }
78
+
79
+ protected function _prepareColumns()
80
+ {
81
+ $this->addColumn('customer_email', array(
82
+ 'header' => Mage::helper('abandonedcarts')->__('Customer Email'),
83
+ 'index' => 'customer_email',
84
+ 'filter_condition_callback' => array($this, 'filterCallback')
85
+ ));
86
+
87
+ $this->addColumn('customer_firstname', array(
88
+ 'header' => Mage::helper('abandonedcarts')->__('Customer Firstname'),
89
+ 'index' => 'customer_firstname',
90
+ 'filter_condition_callback' => array($this, 'filterCallback')
91
+ ));
92
+
93
+ $this->addColumn('customer_lastname', array(
94
+ 'header' => Mage::helper('abandonedcarts')->__('Customer Lastname'),
95
+ 'index' => 'customer_lastname',
96
+ 'filter_condition_callback' => array($this, 'filterCallback')
97
+ ));
98
+
99
+ $this->addColumn('product_ids', array(
100
+ 'header' => Mage::helper('abandonedcarts')->__('Product Ids'),
101
+ 'index' => 'product_ids',
102
+ 'filter_index' => "e.entity_id",
103
+ 'filter_condition_callback' => array($this, 'filterEqualCallback')
104
+ ));
105
+
106
+ $this->addColumn('product_names', array(
107
+ 'header' => Mage::helper('abandonedcarts')->__('Product Names'),
108
+ 'index' => 'product_names',
109
+ 'filter_index' => (Mage::helper('catalog/product_flat')->isEnabled() ? "catalog_flat.name" : "catalog_name.value"),
110
+ 'filter_condition_callback' => array($this, 'filterEqualCallback')
111
+ ));
112
+
113
+ $this->addColumn('product_prices', array(
114
+ 'header' => Mage::helper('abandonedcarts')->__('Cart Regular Total'),
115
+ 'index' => 'product_prices',
116
+ 'filter' => false
117
+ ));
118
+
119
+ $this->addColumn('product_special_prices', array(
120
+ 'header' => Mage::helper('abandonedcarts')->__('Cart Sale Total'),
121
+ 'index' => 'product_special_prices',
122
+ 'filter' => false
123
+ ));
124
+
125
+ // Output format for the start and end dates
126
+ $outputFormat = Mage::app()->getLocale()->getDateTimeFormat(Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM);
127
+
128
+ $this->addColumn('cart_updated_at', array(
129
+ 'header' => Mage::helper('abandonedcarts')->__('Cart Updated At'),
130
+ 'index' => 'cart_updated_at',
131
+ 'type' => 'datetime',
132
+ 'format' => $outputFormat,
133
+ 'default' => ' -- ',
134
+ 'filter_index' => 'quote_table.updated_at',
135
+ 'filter_condition_callback' => array($this, 'filterDateCallback')
136
+ ));
137
+
138
+ return parent::_prepareColumns();
139
+ }
140
+
141
+ /**
142
+ * @return $this
143
+ */
144
+ protected function _prepareMassaction()
145
+ {
146
+ $this->setMassactionIdField('customer_email');
147
+ $this->getMassactionBlock()->setFormFieldName('abandonedcarts');
148
+
149
+ $this->getMassactionBlock()->addItem('notifySale', array(
150
+ 'label' => Mage::helper('abandonedcarts')->__('Send notification'),
151
+ 'url' => $this->getUrl('*/*/notifySale')
152
+ ));
153
+
154
+ return $this;
155
+ }
156
+
157
+ /**
158
+ * @return array
159
+ */
160
+ protected function _getDefaultStoreAndWebsite()
161
+ {
162
+ foreach (Mage::app()->getWebsites() as $website) {
163
+ // Get the website id
164
+ $websiteId = $website->getWebsiteId();
165
+ foreach ($website->getGroups() as $group) {
166
+ $stores = $group->getStores();
167
+ foreach ($stores as $store) {
168
+
169
+ // Get the store id
170
+ $storeId = $store->getStoreId();
171
+ break 3;
172
+ }
173
+ }
174
+ }
175
+ return array($storeId, $websiteId);
176
+ }
177
+
178
+ /**
179
+ * @return string
180
+ */
181
+ public function getGridUrl()
182
+ {
183
+ return $this->getUrl('*/*/salegrid', array('current' => true));
184
+ }
185
+
186
+ /**
187
+ * @param $collection
188
+ * @param $column
189
+ */
190
+ public function filterCallback($collection, $column)
191
+ {
192
+ $field = $column->getFilterIndex() ? $column->getFilterIndex() : $column->getIndex();
193
+ $value = $column->getFilter()->getValue();
194
+ $collection->getSelect()->where("$field like ?", '%' . $value . '%');
195
+ }
196
+
197
+ /**
198
+ * @param $collection
199
+ * @param $column
200
+ */
201
+ public function filterEqualCallback($collection, $column)
202
+ {
203
+ $field = $column->getFilterIndex() ? $column->getFilterIndex() : $column->getIndex();
204
+ $value = $column->getFilter()->getValue();
205
+ $collection->getSelect()->where("$field = ?", $value);
206
+ }
207
+
208
+ /**
209
+ * @param $collection
210
+ * @param $column
211
+ */
212
+ public function filterDateCallback($collection, $column)
213
+ {
214
+ $field = $column->getFilterIndex() ? $column->getFilterIndex() : $column->getIndex();
215
+ $value = $column->getFilter()->getValue();
216
+ $collection->getSelect()->where("$field > '" . $value['from']->toString('Y-MM-dd HH:mm:ss') . "' AND $field < '" . $value['to']->toString('Y-MM-dd HH:mm:ss') . "'");
217
+ }
218
+ }
app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/System/Config/Form/Button.php DELETED
@@ -1,54 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Class DigitalPianism_Abandonedcarts_Block_Adminhtml_System_Config_Form_Button
5
- */
6
- class DigitalPianism_Abandonedcarts_Block_Adminhtml_System_Config_Form_Button extends Mage_Adminhtml_Block_System_Config_Form_Field
7
- {
8
- /*
9
- * Set template
10
- */
11
- protected function _construct()
12
- {
13
- parent::_construct();
14
- $this->setTemplate('digitalpianism/abandonedcarts/system/config/button.phtml');
15
- }
16
-
17
- /**
18
- * Return element html
19
- *
20
- * @param Varien_Data_Form_Element_Abstract $element
21
- * @return string
22
- */
23
- protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
24
- {
25
- return $this->_toHtml();
26
- }
27
-
28
- /**
29
- * Return ajax url for button
30
- *
31
- * @return string
32
- */
33
- public function getAjaxCheckUrl()
34
- {
35
- return Mage::helper('adminhtml')->getUrl('adminhtml/abandonedcarts/send');
36
- }
37
-
38
- /**
39
- * Generate button html
40
- *
41
- * @return string
42
- */
43
- public function getButtonHtml()
44
- {
45
- $button = $this->getLayout()->createBlock('adminhtml/widget_button')
46
- ->setData(array(
47
- 'id' => 'abandonedcarts_button',
48
- 'label' => $this->helper('adminhtml')->__('Send'),
49
- 'onclick' => 'javascript:send(); return false;'
50
- ));
51
-
52
- return $button->toHtml();
53
- }
54
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/code/community/DigitalPianism/Abandonedcarts/Helper/Data.php CHANGED
@@ -6,54 +6,86 @@
6
  class DigitalPianism_Abandonedcarts_Helper_Data extends Mage_Core_Helper_Abstract
7
  {
8
  protected $logFileName = 'digitalpianism_abandonedcarts.log';
9
-
10
  /**
11
  * Log data
12
  * @param string|object|array data to log
13
  */
14
- public function log($data)
15
  {
16
  Mage::log($data, null, $this->logFileName);
17
  }
18
 
19
- /**
20
- * @return mixed
21
- */
22
- public function isEnabled()
23
  {
24
- return Mage::getStoreConfig('abandonedcartsconfig/options/enable');
25
  }
26
 
27
- /**
28
- * @return mixed
29
- */
30
- public function isSaleEnabled()
31
  {
32
- return Mage::getStoreConfig('abandonedcartsconfig/options/enable_sale');
33
  }
34
 
35
- /**
36
- * @return mixed
37
- */
38
- public function getDryRun()
39
  {
40
- return Mage::getStoreConfig('abandonedcartsconfig/options/dryrun');
41
  }
42
 
43
- /**
44
- * @return mixed
45
- */
46
- public function getTestEmail()
47
  {
48
- return Mage::getStoreConfig('abandonedcartsconfig/options/testemail');
49
  }
50
 
51
- /**
52
- * @return mixed
53
- */
54
- public function getCustomerGroupsLimitation()
55
  {
56
  return explode(',',Mage::getStoreConfig('abandonedcartsconfig/options/customer_groups'));
57
  }
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  }
6
  class DigitalPianism_Abandonedcarts_Helper_Data extends Mage_Core_Helper_Abstract
7
  {
8
  protected $logFileName = 'digitalpianism_abandonedcarts.log';
9
+
10
  /**
11
  * Log data
12
  * @param string|object|array data to log
13
  */
14
+ public function log($data)
15
  {
16
  Mage::log($data, null, $this->logFileName);
17
  }
18
 
19
+ /**
20
+ * @return mixed
21
+ */
22
+ public function isEnabled()
23
  {
24
+ return Mage::getStoreConfigFlag('abandonedcartsconfig/options/enable');
25
  }
26
 
27
+ /**
28
+ * @return mixed
29
+ */
30
+ public function isSaleEnabled()
31
  {
32
+ return Mage::getStoreConfigFlag('abandonedcartsconfig/options/enable_sale');
33
  }
34
 
35
+ /**
36
+ * @return mixed
37
+ */
38
+ public function getDryRun()
39
  {
40
+ return Mage::getStoreConfigFlag('abandonedcartsconfig/test/dryrun');
41
  }
42
 
43
+ /**
44
+ * @return mixed
45
+ */
46
+ public function getTestEmail()
47
  {
48
+ return Mage::getStoreConfig('abandonedcartsconfig/test/testemail');
49
  }
50
 
51
+ /**
52
+ * @return mixed
53
+ */
54
+ public function getCustomerGroupsLimitation()
55
  {
56
  return explode(',',Mage::getStoreConfig('abandonedcartsconfig/options/customer_groups'));
57
  }
58
 
59
+ /**
60
+ * @return bool
61
+ */
62
+ public function isCampaignEnabled()
63
+ {
64
+ return Mage::getStoreConfigFlag('abandonedcartsconfig/campaign/enable');
65
+ }
66
+
67
+ /**
68
+ * @return mixed
69
+ */
70
+ public function getCampaignName()
71
+ {
72
+ return Mage::getStoreConfig('abandonedcartsconfig/campaign/name');
73
+ }
74
+
75
+ /**
76
+ * @return bool
77
+ */
78
+ public function isAutologin()
79
+ {
80
+ return Mage::getStoreConfigFlag('abandonedcartsconfig/email/autologin');
81
+ }
82
+
83
+ /**
84
+ * @return bool
85
+ */
86
+ public function isLogEnabled()
87
+ {
88
+ return Mage::getStoreConfigFlag('abandonedcartsconfig/test/log');
89
+ }
90
+
91
  }
app/code/community/DigitalPianism/Abandonedcarts/Model/Adminhtml/Observer.php ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Model_Adminhtml_Observer
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Model_Adminhtml_Observer {
7
+
8
+ /**
9
+ * @param Varien_Event_Observer $observer
10
+ * @return $this
11
+ */
12
+ public function registerController(Varien_Event_Observer $observer)
13
+ {
14
+ $action = $observer->getControllerAction()->getFullActionName();
15
+
16
+ switch ($action)
17
+ {
18
+ case 'adminhtml_report_shopcart_abandoned':
19
+ case 'adminhtml_report_shopcart_exportAbandonedCsv':
20
+ case 'adminhtml_report_shopcart_exportAbandonedExcel':
21
+ Mage::register('abandonedcart_report', true);
22
+ break;
23
+ }
24
+
25
+ return $this;
26
+ }
27
+
28
+ /**
29
+ * @param Varien_Event_Observer $observer
30
+ */
31
+ public function addExtraColumnsToGrid(Varien_Event_Observer $observer)
32
+ {
33
+ // Get the block
34
+ $block = $observer->getBlock();
35
+
36
+ if ($block instanceof Mage_Adminhtml_Block_Report_Shopcart_Abandoned_Grid) {
37
+ $block->addColumnAfter(
38
+ 'abandoned_notified',
39
+ array(
40
+ 'header' => Mage::helper('abandonedcarts')->__('Abandoned cart email sent'),
41
+ 'index' => 'abandoned_notified',
42
+ 'type' => 'options',
43
+ 'options' => array(
44
+ 0 => Mage::helper('abandonedcarts')->__('No'),
45
+ 1 => Mage::helper('abandonedcarts')->__('Yes')
46
+ )
47
+ ),
48
+ 'remote_ip'
49
+ );
50
+
51
+ $block->addColumnAfter(
52
+ 'abandoned_sale_notified',
53
+ array(
54
+ 'header' => Mage::helper('abandonedcarts')->__('Abandoned cart sale email sent'),
55
+ 'index' => 'abandoned_sale_notified',
56
+ 'type' => 'options',
57
+ 'options' => array(
58
+ 0 => Mage::helper('abandonedcarts')->__('No'),
59
+ 1 => Mage::helper('abandonedcarts')->__('Yes')
60
+ )
61
+ ),
62
+ 'abandoned_notified'
63
+ );
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param Varien_Event_Observer $observer
69
+ */
70
+ public function addExtraColumnsToCollection(Varien_Event_Observer $observer)
71
+ {
72
+ // Get the collection
73
+ $collection = $observer->getCollection();
74
+
75
+ if ($collection instanceof Mage_Reports_Model_Resource_Quote_Collection
76
+ && Mage::registry('abandonedcart_report')) {
77
+ // Add the extra fields
78
+ // Using columns() instead of addFieldToSelect seems to fix the ambiguous column error
79
+ $collection->getSelect()->columns(
80
+ array(
81
+ 'abandoned_notified' => 'main_table.abandoned_notified',
82
+ 'abandoned_sale_notified' => 'main_table.abandoned_sale_notified'
83
+ )
84
+ );
85
+ }
86
+ }
87
+ }
app/code/community/DigitalPianism/Abandonedcarts/Model/Collection.php ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Model_Collection
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Model_Collection {
7
+
8
+ /**
9
+ * @param $delay
10
+ * @param $storeId
11
+ * @param $websiteId
12
+ * @param $emails
13
+ * @return mixed
14
+ */
15
+ public function getCollection($delay, $storeId, $websiteId, $emails = array())
16
+ {
17
+ // Get the product collection
18
+ $collection = Mage::getResourceModel('catalog/product_collection')->setStore($storeId);
19
+
20
+ // Get the attribute id for the status attribute
21
+ $eavAttribute = Mage::getModel('eav/entity_attribute');
22
+ $statusId = $eavAttribute->getIdByCode('catalog_product', 'status');
23
+ $nameId = $eavAttribute->getIdByCode('catalog_product', 'name');
24
+ $priceId = $eavAttribute->getIdByCode('catalog_product', 'price');
25
+
26
+ // Normal join condition
27
+ $emailJoin = sprintf('quote_items.quote_id = quote_table.entity_id AND quote_table.items_count > 0 AND quote_table.is_active = 1 AND quote_table.customer_email IS NOT NULL AND quote_table.abandoned_notified = 0 AND quote_table.updated_at < "%s" AND quote_table.store_id = %s', $delay, $storeId);
28
+
29
+ // In case an array of emails has been specified
30
+ if (!empty($emails)) {
31
+ $emailJoin = sprintf('%s AND quote_table.customer_email IN (%s)', $emailJoin, '"' . implode('", "', $emails) . '"');
32
+ }
33
+
34
+ // If flat catalog is enabled
35
+ if (Mage::helper('catalog/product_flat')->isEnabled())
36
+ {
37
+ // First collection: carts with products that became on sale
38
+ // Join the collection with the required tables
39
+ $collection->getSelect()
40
+ ->reset(Zend_Db_Select::COLUMNS)
41
+ ->columns(array('e.entity_id AS product_id',
42
+ 'e.sku',
43
+ 'catalog_flat.name as product_name',
44
+ 'catalog_flat.price as product_price',
45
+ 'quote_table.entity_id as cart_id',
46
+ 'quote_table.updated_at as cart_updated_at',
47
+ 'quote_table.abandoned_notified as has_been_notified',
48
+ 'quote_table.customer_email as customer_email',
49
+ 'quote_table.customer_firstname as customer_firstname',
50
+ 'quote_table.customer_lastname as customer_lastname',
51
+ 'customer.group_id as customer_group'
52
+ )
53
+ )
54
+ ->joinInner(
55
+ array('quote_items' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote_item')),
56
+ 'quote_items.product_id = e.entity_id AND quote_items.price > 0.00',
57
+ null)
58
+ ->joinInner(
59
+ array('quote_table' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote')),
60
+ $emailJoin,
61
+ null)
62
+ ->joinInner(
63
+ array('catalog_flat' => Mage::getSingleton("core/resource")->getTableName('catalog_product_flat_'.$storeId)),
64
+ 'catalog_flat.entity_id = e.entity_id',
65
+ null)
66
+ ->joinInner(
67
+ array('catalog_enabled' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_int')),
68
+ 'catalog_enabled.entity_id = e.entity_id AND catalog_enabled.attribute_id = '.$statusId.' AND catalog_enabled.value = 1',
69
+ null)
70
+ ->joinInner(
71
+ array('inventory' => Mage::getSingleton("core/resource")->getTableName('cataloginventory_stock_status')),
72
+ 'inventory.product_id = e.entity_id AND inventory.stock_status = 1 AND website_id = '.$websiteId,
73
+ null)
74
+ ->joinInner(
75
+ array('customer' => Mage::getSingleton("core/resource")->getTableName('customer_entity')),
76
+ 'quote_table.customer_email = customer.email',
77
+ null)
78
+ ->order('quote_table.updated_at DESC');
79
+ }
80
+ else
81
+ {
82
+ // First collection: carts with products that became on sale
83
+ // Join the collection with the required tables
84
+ $collection->getSelect()
85
+ ->reset(Zend_Db_Select::COLUMNS)
86
+ ->columns(array('e.entity_id AS product_id',
87
+ 'e.sku',
88
+ 'catalog_name.value as product_name',
89
+ 'catalog_price.value as product_price',
90
+ 'quote_table.entity_id as cart_id',
91
+ 'quote_table.updated_at as cart_updated_at',
92
+ 'quote_table.abandoned_notified as has_been_notified',
93
+ 'quote_table.customer_email as customer_email',
94
+ 'quote_table.customer_firstname as customer_firstname',
95
+ 'quote_table.customer_lastname as customer_lastname',
96
+ 'customer.group_id as customer_group'
97
+ )
98
+ )
99
+ // Name
100
+ ->joinInner(
101
+ array('catalog_name' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_varchar')),
102
+ "catalog_name.entity_id = e.entity_id AND catalog_name.attribute_id = $nameId",
103
+ null)
104
+ // Price
105
+ ->joinInner(
106
+ array('catalog_price' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_decimal')),
107
+ "catalog_price.entity_id = e.entity_id AND catalog_price.attribute_id = $priceId",
108
+ null)
109
+ ->joinInner(
110
+ array('quote_items' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote_item')),
111
+ 'quote_items.product_id = e.entity_id AND quote_items.price > 0.00',
112
+ null)
113
+ ->joinInner(
114
+ array('quote_table' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote')),
115
+ $emailJoin,
116
+ null)
117
+ ->joinInner(
118
+ array('catalog_enabled' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_int')),
119
+ 'catalog_enabled.entity_id = e.entity_id AND catalog_enabled.attribute_id = '.$statusId.' AND catalog_enabled.value = 1',
120
+ null)
121
+ ->joinInner(
122
+ array('inventory' => Mage::getSingleton("core/resource")->getTableName('cataloginventory_stock_status')),
123
+ 'inventory.product_id = e.entity_id AND inventory.stock_status = 1 AND website_id = '.$websiteId,
124
+ null)
125
+ ->joinInner(
126
+ array('customer' => Mage::getSingleton("core/resource")->getTableName('customer_entity')),
127
+ 'quote_table.customer_email = customer.email',
128
+ null)
129
+ ->order('quote_table.updated_at DESC');
130
+ }
131
+
132
+ return $collection;
133
+ }
134
+
135
+ public function getSalesCollection($storeId, $websiteId, $emails = array())
136
+ {
137
+ // Get the product collection
138
+ $collection = Mage::getResourceModel('catalog/product_collection')->setStore($storeId);
139
+
140
+ // Get the attribute id for the status attribute
141
+ $eavAttribute = Mage::getModel('eav/entity_attribute');
142
+ $statusId = $eavAttribute->getIdByCode('catalog_product', 'status');
143
+ $nameId = $eavAttribute->getIdByCode('catalog_product', 'name');
144
+ $priceId = $eavAttribute->getIdByCode('catalog_product', 'price');
145
+ $spriceId = $eavAttribute->getIdByCode('catalog_product', 'special_price');
146
+ $spfromId = $eavAttribute->getIdByCode('catalog_product', 'special_from_date');
147
+ $sptoId = $eavAttribute->getIdByCode('catalog_product', 'special_to_date');
148
+
149
+ // Normal join condition
150
+ $emailJoin = sprintf('quote_items.quote_id = quote_table.entity_id AND quote_table.items_count > 0 AND quote_table.is_active = 1 AND quote_table.customer_email IS NOT NULL AND quote_table.abandoned_sale_notified = 0 AND quote_table.store_id = %s', $storeId);
151
+
152
+ // In case an array of emails has been specified
153
+ if (!empty($emails)) {
154
+ $emailJoin = sprintf('%s AND quote_table.customer_email IN (%s)', $emailJoin, '"' . implode('", "', $emails) . '"');
155
+ }
156
+
157
+ // If flat catalog is enabled
158
+ if (Mage::helper('catalog/product_flat')->isEnabled())
159
+ {
160
+ // First collection: carts with products that became on sale
161
+ // Join the collection with the required tables
162
+ $collection->getSelect()
163
+ ->reset(Zend_Db_Select::COLUMNS)
164
+ ->columns(array('e.entity_id AS product_id',
165
+ 'e.sku',
166
+ 'catalog_flat.name as product_name',
167
+ 'catalog_flat.price as product_price',
168
+ 'catalog_flat.special_price as product_special_price',
169
+ 'catalog_flat.special_from_date as product_special_from_date',
170
+ 'catalog_flat.special_to_date as product_special_to_date',
171
+ 'quote_table.entity_id as cart_id',
172
+ 'quote_table.updated_at as cart_updated_at',
173
+ 'quote_table.abandoned_sale_notified as has_been_notified',
174
+ 'quote_items.price as product_price_in_cart',
175
+ 'quote_table.customer_email as customer_email',
176
+ 'quote_table.customer_firstname as customer_firstname',
177
+ 'quote_table.customer_lastname as customer_lastname',
178
+ 'customer.group_id as customer_group'
179
+ )
180
+ )
181
+ ->joinInner(
182
+ array('quote_items' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote_item')),
183
+ 'quote_items.product_id = e.entity_id AND quote_items.price > 0.00',
184
+ null)
185
+ ->joinInner(
186
+ array('quote_table' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote')),
187
+ $emailJoin,
188
+ null)
189
+ ->joinInner(
190
+ array('catalog_flat' => Mage::getSingleton("core/resource")->getTableName('catalog_product_flat_'.$storeId)),
191
+ 'catalog_flat.entity_id = e.entity_id',
192
+ null)
193
+ ->joinInner(
194
+ array('catalog_enabled' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_int')),
195
+ 'catalog_enabled.entity_id = e.entity_id AND catalog_enabled.attribute_id = '.$statusId.' AND catalog_enabled.value = 1',
196
+ null)
197
+ ->joinInner(
198
+ array('inventory' => Mage::getSingleton("core/resource")->getTableName('cataloginventory_stock_status')),
199
+ 'inventory.product_id = e.entity_id AND inventory.stock_status = 1 AND inventory.website_id = '.$websiteId,
200
+ null)
201
+ ->joinInner(
202
+ array('customer' => Mage::getSingleton("core/resource")->getTableName('customer_entity')),
203
+ 'quote_table.customer_email = customer.email',
204
+ null)
205
+ ->order('quote_table.updated_at DESC');
206
+ }
207
+ else
208
+ {
209
+ // First collection: carts with products that became on sale
210
+ // Join the collection with the required tables
211
+ $collection->getSelect()
212
+ ->reset(Zend_Db_Select::COLUMNS)
213
+ ->columns(array('e.entity_id AS product_id',
214
+ 'e.sku',
215
+ 'catalog_name.value as product_name',
216
+ 'catalog_price.value as product_price',
217
+ 'catalog_sprice.value as product_special_price',
218
+ 'catalog_spfrom.value as product_special_from_date',
219
+ 'catalog_spto.value as product_special_to_date',
220
+ 'quote_table.entity_id as cart_id',
221
+ 'quote_table.updated_at as cart_updated_at',
222
+ 'quote_table.abandoned_sale_notified as has_been_notified',
223
+ 'quote_items.price as product_price_in_cart',
224
+ 'quote_table.customer_email as customer_email',
225
+ 'quote_table.customer_firstname as customer_firstname',
226
+ 'quote_table.customer_lastname as customer_lastname',
227
+ 'customer.group_id as customer_group'
228
+ )
229
+ )
230
+ // Name
231
+ ->joinInner(
232
+ array('catalog_name' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_varchar')),
233
+ "catalog_name.entity_id = e.entity_id AND catalog_name.attribute_id = $nameId",
234
+ null)
235
+ // Price
236
+ ->joinInner(
237
+ array('catalog_price' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_decimal')),
238
+ "catalog_price.entity_id = e.entity_id AND catalog_price.attribute_id = $priceId",
239
+ null)
240
+ // Special Price
241
+ ->joinInner(
242
+ array('catalog_sprice' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_decimal')),
243
+ "catalog_sprice.entity_id = e.entity_id AND catalog_sprice.attribute_id = $spriceId",
244
+ null)
245
+ // Special From Date
246
+ ->joinInner(
247
+ array('catalog_spfrom' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_datetime')),
248
+ "catalog_spfrom.entity_id = e.entity_id AND catalog_spfrom.attribute_id = $spfromId",
249
+ null)
250
+ // Special To Date
251
+ ->joinInner(
252
+ array('catalog_spto' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_datetime')),
253
+ "catalog_spto.entity_id = e.entity_id AND catalog_spto.attribute_id = $sptoId",
254
+ null)
255
+ ->joinInner(
256
+ array('quote_items' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote_item')),
257
+ 'quote_items.product_id = e.entity_id AND quote_items.price > 0.00',
258
+ null)
259
+ ->joinInner(
260
+ array('quote_table' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote')),
261
+ $emailJoin,
262
+ null)
263
+ ->joinInner(
264
+ array('catalog_enabled' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_int')),
265
+ 'catalog_enabled.entity_id = e.entity_id AND catalog_enabled.attribute_id = '.$statusId.' AND catalog_enabled.value = 1',
266
+ null)
267
+ ->joinInner(
268
+ array('inventory' => Mage::getSingleton("core/resource")->getTableName('cataloginventory_stock_status')),
269
+ 'inventory.product_id = e.entity_id AND inventory.stock_status = 1 AND inventory.website_id = '.$websiteId,
270
+ null)
271
+ ->joinInner(
272
+ array('customer' => Mage::getSingleton("core/resource")->getTableName('customer_entity')),
273
+ 'quote_table.customer_email = customer.email',
274
+ null)
275
+ ->order('quote_table.updated_at DESC');
276
+ }
277
+
278
+ return $collection;
279
+ }
280
+ }
app/code/community/DigitalPianism/Abandonedcarts/Model/Link.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Model_Link
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Model_Link extends Mage_Core_Model_Abstract
7
+ {
8
+
9
+ protected function _construct()
10
+ {
11
+ $this->_init('abandonedcarts/link', 'link_id');
12
+ }
13
+
14
+ }
app/code/community/DigitalPianism/Abandonedcarts/Model/Link/Cleaner.php ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Model_Link_Cleaner
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Model_Link_Cleaner {
7
+
8
+ /**
9
+ *
10
+ */
11
+ public function cleanExpiredLinks()
12
+ {
13
+ $now = new Zend_Date(Mage::getModel('core/date')->timestamp());
14
+
15
+ // Get the collection of links expired
16
+ $collection = Mage::getResourceModel('abandonedcarts/link_collection')
17
+ ->addFieldToSelect('link_id')
18
+ ->addFieldToFilter('expiration_date', array(
19
+ 'lteq' => $now->toString('YYYY-MM-dd HH:mm:ss')
20
+ )
21
+ );
22
+
23
+ if (!$collection->getSize())
24
+ return;
25
+
26
+ // Delete the expired links
27
+ foreach ($collection as $expiredLink) {
28
+ $expiredLink->delete();
29
+ }
30
+ }
31
+ }
app/code/community/DigitalPianism/Abandonedcarts/Model/Log.php ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Model_Log
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Model_Log extends Mage_Core_Model_Abstract
7
+ {
8
+
9
+ const TYPE_NORMAL = 0;
10
+ const TYPE_SALES = 1;
11
+
12
+ protected function _construct()
13
+ {
14
+ $this->_init('abandonedcarts/log', 'log_id');
15
+ }
16
+
17
+ public function toOptionArray()
18
+ {
19
+ return array(
20
+ self::TYPE_NORMAL => Mage::helper('abandonedcarts')->__('Abandoned cart email'),
21
+ self::TYPE_SALES => Mage::helper('abandonedcarts')->__('Sale abandoned cart email')
22
+ );
23
+ }
24
+
25
+ }
app/code/community/DigitalPianism/Abandonedcarts/Model/Notifier.php ADDED
@@ -0,0 +1,637 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Model_Notifier
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Model_Notifier extends Mage_Core_Model_Abstract
7
+ {
8
+ const IMAGE_SIZE = 250;
9
+ const CAMPAIGN_SOURCE = "abandonedcarts";
10
+ const CAMPAIGN_MEDIUM = "email";
11
+ /**
12
+ * Autologin links expiration in days
13
+ */
14
+ const EXPIRATION = "2";
15
+
16
+ /**
17
+ * @var array
18
+ */
19
+ protected $_recipients = array();
20
+
21
+ /**
22
+ * @var array
23
+ */
24
+ protected $_saleRecipients = array();
25
+
26
+ /**
27
+ * @var string
28
+ */
29
+ protected $_today = "";
30
+
31
+ /**
32
+ * @var array
33
+ */
34
+ protected $_customerGroups = array();
35
+
36
+ /**
37
+ * @var
38
+ */
39
+ protected $_currentStoreId;
40
+
41
+ /**
42
+ * @var
43
+ */
44
+ protected $_originalStoreId;
45
+
46
+ /**
47
+ * @throws Zend_Date_Exception
48
+ */
49
+ protected function _setToday()
50
+ {
51
+ // Date handling
52
+ $store = Mage_Core_Model_App::ADMIN_STORE_ID;
53
+ $timezone = Mage::app()->getStore($store)->getConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE);
54
+ date_default_timezone_set($timezone);
55
+
56
+ // Current date
57
+ $currentdate = date("Ymd");
58
+
59
+ $day = (int)substr($currentdate,-2);
60
+ $month = (int)substr($currentdate,4,2);
61
+ $year = (int)substr($currentdate,0,4);
62
+
63
+ $date = array(
64
+ 'year' => $year,
65
+ 'month' => $month,
66
+ 'day' => $day,
67
+ 'hour' => 23,
68
+ 'minute' => 59,
69
+ 'second' => 59
70
+ );
71
+
72
+ $today = new Zend_Date($date);
73
+ $today->setTimeZone("UTC");
74
+
75
+ date_default_timezone_set($timezone);
76
+
77
+ $this->_today = $today->toString("Y-MM-dd HH:mm:ss");
78
+ }
79
+
80
+ /**
81
+ * @return string
82
+ */
83
+ protected function _getToday()
84
+ {
85
+ return $this->_today;
86
+ }
87
+
88
+ /**
89
+ * @return array
90
+ */
91
+ protected function _getRecipients()
92
+ {
93
+ return $this->_recipients;
94
+ }
95
+
96
+ /**
97
+ * @return array
98
+ */
99
+ protected function _getSaleRecipients()
100
+ {
101
+ return $this->_saleRecipients;
102
+ }
103
+
104
+ protected function _getProductImage($productId)
105
+ {
106
+ // Get product image via collection
107
+ $_productCollection = Mage::getResourceModel('catalog/product_collection');
108
+ // Add attributes to the collection
109
+ $_productCollection->addAttributeToFilter('entity_id',array('eq' => $productId));
110
+ // Add image to the collection
111
+ $_productCollection->joinAttribute('small_image', 'catalog_product/image', 'entity_id', null, 'left');
112
+ // Limit the collection to get the specific product
113
+ $_productCollection->setPageSize(1);
114
+
115
+ try {
116
+ $productImg = (string)Mage::helper('catalog/image')->init($_productCollection->getFirstItem(), 'small_image')->resize(self::IMAGE_SIZE);
117
+ } catch (Exception $e) {
118
+ $productImg = false;
119
+ }
120
+
121
+ return $productImg;
122
+ }
123
+
124
+ /**
125
+ * @param $args
126
+ */
127
+ public function generateRecipients($args)
128
+ {
129
+ // Customer group check
130
+ if (array_key_exists('customer_group',$args['row'])
131
+ && !in_array($args['row']['customer_group'],$this->_customerGroups)) {
132
+ return;
133
+ }
134
+
135
+ // Test if the customer is already in the array
136
+ if (!array_key_exists($args['row']['customer_email'], $this->_recipients)) {
137
+ // Create an array of variables to assign to template
138
+ $emailTemplateVariables = array();
139
+
140
+ // Array that contains the data which will be used inside the template
141
+ $emailTemplateVariables['fullname'] = $args['row']['customer_firstname'].' '.$args['row']['customer_lastname'];
142
+ $emailTemplateVariables['firstname'] = $args['row']['customer_firstname'];
143
+ $emailTemplateVariables['productname'][] = $args['row']['product_name'];
144
+
145
+ // Assign the values to the array of recipients
146
+ $this->_recipients[$args['row']['customer_email']]['cartId'] = $args['row']['cart_id'];
147
+
148
+ // Add product image
149
+ $emailTemplateVariables['productimage'][] = $this->_getProductImage($args['row']['product_id']);
150
+
151
+ // Add the link
152
+ $token = "";
153
+ // Autologin only applies to real customer (skip not logged in customer group)
154
+ if (Mage::helper('abandonedcarts')->isAutologin()) {
155
+ $token = $this->_generateToken($args['row']['customer_email']);
156
+ }
157
+ $emailTemplateVariables['link'] = $this->_generateUrl($token);
158
+ } else {
159
+ // We create some extra variables if there is several products in the cart
160
+ $emailTemplateVariables = $this->_recipients[$args['row']['customer_email']]['emailTemplateVariables'];
161
+ // We increase the product count
162
+ //$emailTemplateVariables['extraproductcount'] += 1;
163
+ $emailTemplateVariables['productname'][] = $args['row']['product_name'];
164
+
165
+ // Add product image
166
+ $emailTemplateVariables['productimage'][] = $this->_getProductImage($args['row']['product_id']);
167
+ }
168
+
169
+ // Assign the array of template variables
170
+ $this->_recipients[$args['row']['customer_email']]['emailTemplateVariables'] = $emailTemplateVariables;
171
+ $this->_recipients[$args['row']['customer_email']]['store_id'] = $this->_currentStoreId;
172
+ }
173
+
174
+ /**
175
+ * @param $args
176
+ */
177
+ public function generateSaleRecipients($args)
178
+ {
179
+ // Customer group check
180
+ if (array_key_exists('customer_group',$args['row'])
181
+ && !in_array($args['row']['customer_group'],$this->_customerGroups)) {
182
+
183
+ return;
184
+ }
185
+
186
+ // Double check if the special from date is set
187
+ if (!array_key_exists('product_special_from_date',$args['row'])
188
+ || !$args['row']['product_special_from_date']) {
189
+
190
+ // If not we use today for the comparison
191
+ $fromDate = $this->_getToday();
192
+ } else {
193
+ $fromDate = $args['row']['product_special_from_date'];
194
+ }
195
+
196
+ // Do the same for the special to date
197
+ if (!array_key_exists('product_special_to_date',$args['row'])
198
+ || !$args['row']['product_special_to_date']) {
199
+
200
+ $toDate = $this->_getToday();
201
+ } else {
202
+ $toDate = $args['row']['product_special_to_date'];
203
+ }
204
+
205
+ // We need to ensure that the price in cart is higher than the new special price
206
+ // As well as the date comparison in case the sale is over or hasn't started
207
+ if ($args['row']['product_price_in_cart'] > 0.00
208
+ && $args['row']['product_special_price'] > 0.00
209
+ && ($args['row']['product_price_in_cart'] > $args['row']['product_special_price'])
210
+ && ($fromDate <= $this->_getToday())
211
+ && ($toDate >= $this->_getToday())) {
212
+
213
+ // Test if the customer is already in the array
214
+ if (!array_key_exists($args['row']['customer_email'], $this->_saleRecipients)) {
215
+
216
+ // Create an array of variables to assign to template
217
+ $emailTemplateVariables = array();
218
+
219
+ // Array that contains the data which will be used inside the template
220
+ $emailTemplateVariables['fullname'] = $args['row']['customer_firstname'].' '.$args['row']['customer_lastname'];
221
+ $emailTemplateVariables['firstname'] = $args['row']['customer_firstname'];
222
+ $emailTemplateVariables['productname'][] = $args['row']['product_name'];
223
+ $emailTemplateVariables['cartprice'][] = Mage::helper('core')->currency(floatval(number_format(floatval($args['row']['product_price_in_cart']),2)), true, false);
224
+ $emailTemplateVariables['specialprice'][] = Mage::helper('core')->currency(floatval(number_format(floatval($args['row']['product_special_price']),2)), true, false);
225
+
226
+ // Assign the values to the array of recipients
227
+ $this->_saleRecipients[$args['row']['customer_email']]['cartId'] = $args['row']['cart_id'];
228
+
229
+ // Add product image
230
+ $emailTemplateVariables['productimage'][] = $this->_getProductImage($args['row']['product_id']);
231
+
232
+ // Add the link
233
+ $token = "";
234
+ // Autologin only applies to real customer (skip not logged in customer group)
235
+ if (Mage::helper('abandonedcarts')->isAutologin()) {
236
+ $token = $this->_generateToken($args['row']['customer_email']);
237
+ }
238
+ $emailTemplateVariables['link'] = $this->_generateUrl($token);
239
+
240
+ // If one product before
241
+ $emailTemplateVariables['discount'] = number_format(floatval($args['row']['product_price_in_cart']),2) - number_format(floatval($args['row']['product_special_price']),2);
242
+ } else {
243
+ // We create some extra variables if there is several products in the cart
244
+ $emailTemplateVariables = $this->_saleRecipients[$args['row']['customer_email']]['emailTemplateVariables'];
245
+ // Discount amount
246
+ // We add the discount on the second product
247
+ $moreDiscount = number_format(floatval($args['row']['product_price_in_cart']),2) - number_format(floatval($args['row']['product_special_price']),2);
248
+ $emailTemplateVariables['discount'] += $moreDiscount;
249
+
250
+ $emailTemplateVariables['productname'][] = $args['row']['product_name'];
251
+ $emailTemplateVariables['cartprice'][] = Mage::helper('core')->currency(floatval(number_format(floatval($args['row']['product_price_in_cart']),2)), true, false);
252
+ $emailTemplateVariables['specialprice'][] = Mage::helper('core')->currency(floatval(number_format(floatval($args['row']['product_special_price']),2)), true, false);
253
+
254
+ // Add product image
255
+ $emailTemplateVariables['productimage'][] = $this->_getProductImage($args['row']['product_id']);
256
+ }
257
+
258
+ // Assign the array of template variables
259
+ $this->_saleRecipients[$args['row']['customer_email']]['emailTemplateVariables'] = $emailTemplateVariables;
260
+ $this->_saleRecipients[$args['row']['customer_email']]['store_id'] = $this->_currentStoreId;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * @param $dryrun
266
+ * @param $testemail
267
+ * @throws Exception
268
+ */
269
+ protected function _sendSaleEmails($dryrun, $testemail)
270
+ {
271
+ // Send the emails via a loop
272
+ foreach ($this->_getSaleRecipients() as $email => $recipient) {
273
+
274
+ // Store Id
275
+ Mage::app()->setCurrentStore($recipient['store_id']);
276
+ // Get the transactional email template
277
+ $templateId = Mage::getStoreConfig('abandonedcartsconfig/email/email_template_sale');
278
+ // Get the sender
279
+ $sender = array();
280
+ $sender['email'] = Mage::getStoreConfig('abandonedcartsconfig/email/email');
281
+ $sender['name'] = Mage::getStoreConfig('abandonedcartsconfig/email/name');
282
+ $recipient['emailTemplateVariables']['email'] = $email;
283
+
284
+ // Format discount with currency
285
+ $recipient['emailTemplateVariables']['discount'] = Mage::helper('core')->currency($recipient['emailTemplateVariables']['discount'], true, false);
286
+
287
+ // Don't send the email if dryrun is set
288
+ if ($dryrun) {
289
+ // If the test email is set and found
290
+ if (isset($testemail)) {
291
+ // Send to the test email
292
+ Mage::getModel('core/email_template')
293
+ ->sendTransactional(
294
+ $templateId,
295
+ $sender,
296
+ $testemail,
297
+ $recipient['emailTemplateVariables']['fullname'] ,
298
+ $recipient['emailTemplateVariables'],
299
+ null);
300
+ }
301
+ } else {
302
+ // Send the email
303
+ Mage::getModel('core/email_template')
304
+ ->sendTransactional(
305
+ $templateId,
306
+ $sender,
307
+ $email,
308
+ $recipient['emailTemplateVariables']['fullname'] ,
309
+ $recipient['emailTemplateVariables'],
310
+ null);
311
+ }
312
+
313
+ if (Mage::helper('abandonedcarts')->isLogEnabled()) {
314
+ // Log the details
315
+ $comment = sprintf(
316
+ "Email sent to %s, product name: %s, cart price: %s, special price: %s, discount: %s, product image: %s, link: %s",
317
+ $recipient['emailTemplateVariables']['fullname'],
318
+ implode(',', $recipient['emailTemplateVariables']['productname']),
319
+ implode(',', $recipient['emailTemplateVariables']['cartprice']),
320
+ implode(',', $recipient['emailTemplateVariables']['specialprice']),
321
+ $recipient['emailTemplateVariables']['discount'],
322
+ implode(',', $recipient['emailTemplateVariables']['productimage']),
323
+ $recipient['emailTemplateVariables']['link']
324
+ );
325
+
326
+ Mage::getModel('abandonedcarts/log')->setData(
327
+ array(
328
+ 'customer_email' => $email,
329
+ 'type' => DigitalPianism_Abandonedcarts_Model_Log::TYPE_SALES,
330
+ 'comment' => $comment,
331
+ 'store' => $recipient['store_id'],
332
+ 'dryrun' => $dryrun ? 1 : 0
333
+ )
334
+ )->save();
335
+ }
336
+
337
+ // Save only if dryrun is false
338
+ if (!$dryrun) {
339
+
340
+ // Load the quote
341
+ $quote = Mage::getModel('sales/quote')->load($recipient['cartId']);
342
+
343
+ // We change the notification attribute
344
+ $quote->setAbandonedSaleNotified(1);
345
+
346
+ $quote->getResource()->saveAttribute($quote,array('abandoned_sale_notified'));
347
+ }
348
+ }
349
+ }
350
+
351
+ /**
352
+ * @param $dryrun
353
+ * @param $testemail
354
+ * @throws Exception
355
+ */
356
+ protected function _sendEmails($dryrun, $testemail)
357
+ {
358
+ // Send the emails via a loop
359
+ foreach ($this->_getRecipients() as $email => $recipient) {
360
+
361
+ // Store ID
362
+ Mage::app()->setCurrentStore($recipient['store_id']);
363
+ // Get the transactional email template
364
+ $templateId = Mage::getStoreConfig('abandonedcartsconfig/email/email_template');
365
+ // Get the sender
366
+ $sender = array();
367
+ $sender['email'] = Mage::getStoreConfig('abandonedcartsconfig/email/email');
368
+ $sender['name'] = Mage::getStoreConfig('abandonedcartsconfig/email/name');
369
+ $recipient['emailTemplateVariables']['email'] = $email;
370
+
371
+ // Don't send the email if dryrun is set
372
+ if ($dryrun) {
373
+ // If the test email is set and found
374
+ if (isset($testemail)) {
375
+ // Send the email to the test email
376
+ Mage::getModel('core/email_template')
377
+ ->sendTransactional(
378
+ $templateId,
379
+ $sender,
380
+ $testemail,
381
+ $recipient['emailTemplateVariables']['fullname'] ,
382
+ $recipient['emailTemplateVariables'],
383
+ null);
384
+ }
385
+ } else {
386
+ // Send the email
387
+ Mage::getModel('core/email_template')
388
+ ->sendTransactional(
389
+ $templateId,
390
+ $sender,
391
+ $email,
392
+ $recipient['emailTemplateVariables']['fullname'] ,
393
+ $recipient['emailTemplateVariables'],
394
+ null);
395
+ }
396
+
397
+ if (Mage::helper('abandonedcarts')->isLogEnabled()) {
398
+ // Log the details
399
+ $comment = sprintf(
400
+ //"Email sent to %s, product name: %s, product image: %s, extra product count: %s, link: %s",
401
+ "Email sent to %s, product name: %s, product image: %s, link: %s",
402
+ $recipient['emailTemplateVariables']['fullname'],
403
+ implode(',', $recipient['emailTemplateVariables']['productname']),
404
+ implode(',', $recipient['emailTemplateVariables']['productimage']) ? implode(',', $recipient['emailTemplateVariables']['productimage']) : "none",
405
+ $recipient['emailTemplateVariables']['link']
406
+ );
407
+
408
+ Mage::getModel('abandonedcarts/log')->setData(
409
+ array(
410
+ 'customer_email' => $email,
411
+ 'type' => DigitalPianism_Abandonedcarts_Model_Log::TYPE_NORMAL,
412
+ 'comment' => $comment,
413
+ 'store' => $recipient['store_id'],
414
+ 'dryrun' => $dryrun ? 1 : 0
415
+ )
416
+ )->save();
417
+ }
418
+
419
+ // Save only if dryrun is false
420
+ if (!$dryrun) {
421
+ // Load the quote
422
+ $quote = Mage::getModel('sales/quote')->load($recipient['cartId']);
423
+
424
+ // We change the notification attribute
425
+ $quote->setAbandonedNotified(1);
426
+
427
+ $quote->getResource()->saveAttribute($quote,array('abandoned_notified'));
428
+ }
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Send notification email to customer with abandoned cart containing sale products
434
+ * If dryrun is set to true, it won't send emails and won't alter quotes
435
+ * @param boolean
436
+ * @param string
437
+ */
438
+ public function sendAbandonedCartsSaleEmail($dryrun = false, $testemail = null, $emails = array())
439
+ {
440
+ if (Mage::helper('abandonedcarts')->getDryRun()) {
441
+ $dryrun = true;
442
+ }
443
+
444
+ if (Mage::helper('abandonedcarts')->getTestEmail()) {
445
+ $testemail = Mage::helper('abandonedcarts')->getTestEmail();
446
+ }
447
+
448
+ // Set customer groups
449
+ $this->_customerGroups = $this->_customerGroups ? $this->_customerGroups : Mage::helper('abandonedcarts')->getCustomerGroupsLimitation();
450
+ // Original store id
451
+ $this->_originalStoreId = Mage::app()->getStore()->getId();
452
+ try
453
+ {
454
+ if (Mage::helper('abandonedcarts')->isSaleEnabled()) {
455
+
456
+ $this->_setToday();
457
+
458
+ // Loop through the stores
459
+ foreach (Mage::app()->getWebsites() as $website) {
460
+ // Get the website id
461
+ $websiteId = $website->getWebsiteId();
462
+ foreach ($website->getGroups() as $group) {
463
+ $stores = $group->getStores();
464
+ foreach ($stores as $store) {
465
+
466
+ // Get the store id
467
+ $storeId = $store->getStoreId();
468
+ $this->_currentStoreId = $storeId;
469
+
470
+ // Init the store to be able to load the quote and the collections properly
471
+ Mage::app()->init($storeId,'store');
472
+
473
+ // Get the collection
474
+ $collection = Mage::getModel('abandonedcarts/collection')->getSalesCollection($storeId, $websiteId, $emails);
475
+
476
+ //$collection->printlogquery(true,true);
477
+ $collection->load();
478
+
479
+ // Skip the rest of the code if the collection is empty
480
+ if ($collection->getSize() == 0) {
481
+ continue;
482
+ }
483
+
484
+ // Call iterator walk method with collection query string and callback method as parameters
485
+ // Has to be used to handle massive collection instead of foreach
486
+ Mage::getSingleton('core/resource_iterator')->walk($collection->getSelect(), array(array($this, 'generateSaleRecipients')));
487
+ }
488
+ }
489
+ }
490
+ $this->_sendSaleEmails($dryrun, $testemail);
491
+ }
492
+ Mage::app()->setCurrentStore($this->_originalStoreId);
493
+
494
+ return count($this->_getSaleRecipients());
495
+ }
496
+ catch (Exception $e)
497
+ {
498
+ Mage::app()->setCurrentStore($this->_originalStoreId);
499
+ Mage::helper('abandonedcarts')->log(sprintf("%s->Error: %s", __METHOD__, $e->getMessage()));
500
+ return 0;
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Send notification email to customer with abandoned carts after the number of days specified in the config
506
+ * @param bool $nodate
507
+ * @param bool $dryrun
508
+ * @param string $testemail
509
+ * @internal param if $boolean dryrun is set to true, it won't send emails and won't alter quotes
510
+ */
511
+ public function sendAbandonedCartsEmail($nodate = false, $dryrun = false, $testemail = null, $emails = array())
512
+ {
513
+ if (Mage::helper('abandonedcarts')->getDryRun()) {
514
+ $dryrun = true;
515
+ }
516
+
517
+ if (Mage::helper('abandonedcarts')->getTestEmail()) {
518
+ $testemail = Mage::helper('abandonedcarts')->getTestEmail();
519
+ }
520
+
521
+ // Set customer groups
522
+ $this->_customerGroups = $this->_customerGroups ? $this->_customerGroups : Mage::helper('abandonedcarts')->getCustomerGroupsLimitation();
523
+ // Original store id
524
+ $this->_originalStoreId = Mage::app()->getStore()->getId();
525
+ try
526
+ {
527
+ if (Mage::helper('abandonedcarts')->isEnabled()) {
528
+
529
+ // Date handling
530
+ $store = Mage_Core_Model_App::ADMIN_STORE_ID;
531
+ $timezone = Mage::app()->getStore($store)->getConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE);
532
+ date_default_timezone_set($timezone);
533
+
534
+ // If the nodate parameter is set to false
535
+ if (!$nodate) {
536
+ // Get the delay provided and convert it to a proper date
537
+ $delay = Mage::getStoreConfig('abandonedcartsconfig/options/notify_delay');
538
+ $delay = date('Y-m-d H:i:s', time() - $delay * 24 * 3600);
539
+ } else {
540
+ // We create a date in the future to handle all abandoned carts
541
+ $delay = date('Y-m-d H:i:s', strtotime("+7 day"));
542
+ }
543
+
544
+ // Loop through the stores
545
+ foreach (Mage::app()->getWebsites() as $website) {
546
+ // Get the website id
547
+ $websiteId = $website->getWebsiteId();
548
+ foreach ($website->getGroups() as $group) {
549
+ $stores = $group->getStores();
550
+ foreach ($stores as $store) {
551
+
552
+ // Get the store id
553
+ $storeId = $store->getStoreId();
554
+ $this->_currentStoreId = $storeId;
555
+ // Init the store to be able to load the quote and the collections properly
556
+ Mage::app()->init($storeId, 'store');
557
+
558
+ // Get the collection
559
+ $collection = Mage::getModel('abandonedcarts/collection')->getCollection($delay, $storeId, $websiteId, $emails);
560
+
561
+ //$collection->printlogquery(false,true);
562
+ $collection->load();
563
+
564
+ // Skip the rest of the code if the collection is empty
565
+ if ($collection->getSize() == 0) {
566
+ continue;
567
+ }
568
+
569
+ // Call iterator walk method with collection query string and callback method as parameters
570
+ // Has to be used to handle massive collection instead of foreach
571
+ Mage::getSingleton('core/resource_iterator')->walk($collection->getSelect(), array(array($this, 'generateRecipients')));
572
+ }
573
+ }
574
+ }
575
+ // Send the emails
576
+ $this->_sendEmails($dryrun, $testemail);
577
+ }
578
+ Mage::app()->setCurrentStore($this->_originalStoreId);
579
+
580
+ return count($this->_getRecipients());
581
+ }
582
+ catch (Exception $e)
583
+ {
584
+ Mage::app()->setCurrentStore($this->_originalStoreId);
585
+ Mage::helper('abandonedcarts')->log(sprintf("%s->Error: %s", __METHOD__, $e->getMessage()));
586
+ return 0;
587
+ }
588
+ }
589
+
590
+ /**
591
+ * @return mixed|string
592
+ */
593
+ protected function _generateUrl($token = "")
594
+ {
595
+ if (!Mage::helper('abandonedcarts')->isCampaignEnabled()) {
596
+ return Mage::getUrl('abandonedcarts',
597
+ array(
598
+ '_query' => ($token ? "?token=" . $token : ''),
599
+ '_secure' => true
600
+ )
601
+ );
602
+ }
603
+
604
+ return Mage::getUrl('abandonedcarts', array(
605
+ '_query' => "?utm_source=" . self::CAMPAIGN_SOURCE . "&utm_medium=" . self::CAMPAIGN_MEDIUM . "&utm_campaign=" . Mage::helper('abandonedcarts')->getCampaignName() . ($token ? "&token=" . $token : ''),
606
+ '_secure' => true
607
+ )
608
+ );
609
+ }
610
+
611
+ /**
612
+ * @param $customerEmail
613
+ * @return string
614
+ */
615
+ protected function _generateToken($customerEmail)
616
+ {
617
+ // Generate the token
618
+ $token = openssl_random_pseudo_bytes(9, $cstrong);
619
+ // Generate the token hash
620
+ $hash = hash("sha256", $token);
621
+
622
+ // Generate the expiration date
623
+ $expiration = new Zend_Date(Mage::getModel('core/date')->timestamp());
624
+ $expiration->addDay(self::EXPIRATION);
625
+
626
+ // Create the autologin link
627
+ Mage::getModel('abandonedcarts/link')->setData(
628
+ array(
629
+ 'token_hash' => $hash,
630
+ 'customer_email' => $customerEmail,
631
+ 'expiration_date' => $expiration->toString('YYYY-MM-dd HH:mm:ss')
632
+ )
633
+ )->save();
634
+
635
+ return $token;
636
+ }
637
+ }
app/code/community/DigitalPianism/Abandonedcarts/Model/Observer.php DELETED
@@ -1,742 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Class DigitalPianism_Abandonedcarts_Model_Observer
5
- */
6
- class DigitalPianism_Abandonedcarts_Model_Observer extends Mage_Core_Model_Abstract
7
- {
8
-
9
- protected $_recipients = array();
10
- protected $_saleRecipients = array();
11
- protected $_today = "";
12
- protected $_customerGroups = "";
13
- protected $_currentStoreId;
14
- protected $_originalStoreId;
15
-
16
- protected function _setToday()
17
- {
18
- // Date handling
19
- $store = Mage_Core_Model_App::ADMIN_STORE_ID;
20
- $timezone = Mage::app()->getStore($store)->getConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE);
21
- date_default_timezone_set($timezone);
22
-
23
- // Current date
24
- $currentdate = date("Ymd");
25
-
26
- $day = (int)substr($currentdate,-2);
27
- $month = (int)substr($currentdate,4,2);
28
- $year = (int)substr($currentdate,0,4);
29
-
30
- $date = array('year' => $year,'month' => $month,'day' => $day,'hour' => 23,'minute' => 59,'second' => 59);
31
-
32
- $today = new Zend_Date($date);
33
- $today->setTimeZone("UTC");
34
-
35
- date_default_timezone_set($timezone);
36
-
37
- $this->_today = $today->toString("Y-MM-dd HH:mm:ss");
38
- }
39
-
40
- /**
41
- * @return string
42
- */
43
- protected function _getToday()
44
- {
45
- return $this->_today;
46
- }
47
-
48
- /**
49
- * @return array
50
- */
51
- protected function _getRecipients()
52
- {
53
- return $this->_recipients;
54
- }
55
-
56
- /**
57
- * @return array
58
- */
59
- protected function _getSaleRecipients()
60
- {
61
- return $this->_saleRecipients;
62
- }
63
-
64
- /**
65
- * @param $args
66
- */
67
- public function generateRecipients($args)
68
- {
69
- // Customer group check
70
- if (array_key_exists('customer_group',$args['row']) && !in_array($args['row']['customer_group'],$this->_customerGroups))
71
- {
72
- return;
73
- }
74
-
75
- // Test if the customer is already in the array
76
- if (!array_key_exists($args['row']['customer_email'], $this->_recipients))
77
- {
78
- // Create an array of variables to assign to template
79
- $emailTemplateVariables = array();
80
-
81
- // Array that contains the data which will be used inside the template
82
- $emailTemplateVariables['fullname'] = $args['row']['customer_firstname'].' '.$args['row']['customer_lastname'];
83
- $emailTemplateVariables['firstname'] = $args['row']['customer_firstname'];
84
- $emailTemplateVariables['productname'] = $args['row']['product_name'];
85
-
86
- // Assign the values to the array of recipients
87
- $this->_recipients[$args['row']['customer_email']]['cartId'] = $args['row']['cart_id'];
88
-
89
- // Get product image via collection
90
- $_productCollection = Mage::getResourceModel('catalog/product_collection');
91
- // Add attributes to the collection
92
- $_productCollection->addAttributeToFilter('entity_id',array('eq' => $args['row']['product_id']));
93
- // Add image to the collection
94
- $_productCollection->joinAttribute('image', 'catalog_product/image', 'entity_id', null, 'left');
95
- // Limit the collection to get the specific product
96
- $_productCollection->setPageSize(1);
97
-
98
- try {
99
- $productImg = (string)Mage::helper('catalog/image')->init($_productCollection->getFirstItem(), 'image');
100
- }
101
- catch (Exception $e) {
102
- $productImg = false;
103
- }
104
-
105
- // Add product image
106
- $emailTemplateVariables['productimage'] = $productImg;
107
-
108
- $emailTemplateVariables['extraproductcount'] = 0;
109
- }
110
- else
111
- {
112
- // We create some extra variables if there is several products in the cart
113
- $emailTemplateVariables = $this->_recipients[$args['row']['customer_email']]['emailTemplateVariables'];
114
- // We increase the product count
115
- $emailTemplateVariables['extraproductcount'] += 1;
116
- }
117
- // Assign the array of template variables
118
- $this->_recipients[$args['row']['customer_email']]['emailTemplateVariables'] = $emailTemplateVariables;
119
- $this->_recipients[$args['row']['customer_email']]['store_id'] = $this->_currentStoreId;
120
- }
121
-
122
- /**
123
- * @param $args
124
- */
125
- public function generateSaleRecipients($args)
126
- {
127
- // Customer group check
128
- if (array_key_exists('customer_group',$args['row']) && !in_array($args['row']['customer_group'],$this->_customerGroups))
129
- {
130
- return;
131
- }
132
-
133
- // Double check if the special from date is set
134
- if (!array_key_exists('product_special_from_date',$args['row']) || !$args['row']['product_special_from_date'])
135
- {
136
- // If not we use today for the comparison
137
- $fromDate = $this->_getToday();
138
- }
139
- else $fromDate = $args['row']['product_special_from_date'];
140
-
141
- // Do the same for the special to date
142
- if (!array_key_exists('product_special_to_date',$args['row']) || !$args['row']['product_special_to_date'])
143
- {
144
- $toDate = $this->_getToday();
145
- }
146
- else $toDate = $args['row']['product_special_to_date'];
147
-
148
- // We need to ensure that the price in cart is higher than the new special price
149
- // As well as the date comparison in case the sale is over or hasn't started
150
- if ($args['row']['product_price_in_cart'] > 0.00
151
- && $args['row']['product_special_price'] > 0.00
152
- && ($args['row']['product_price_in_cart'] > $args['row']['product_special_price'])
153
- && ($fromDate <= $this->_getToday())
154
- && ($toDate >= $this->_getToday()))
155
- {
156
-
157
- // Test if the customer is already in the array
158
- if (!array_key_exists($args['row']['customer_email'], $this->_saleRecipients))
159
- {
160
- // Create an array of variables to assign to template
161
- $emailTemplateVariables = array();
162
-
163
- // Array that contains the data which will be used inside the template
164
- $emailTemplateVariables['fullname'] = $args['row']['customer_firstname'].' '.$args['row']['customer_lastname'];
165
- $emailTemplateVariables['firstname'] = $args['row']['customer_firstname'];
166
- $emailTemplateVariables['productname'] = $args['row']['product_name'];
167
- $emailTemplateVariables['cartprice'] = number_format($args['row']['product_price_in_cart'],2);
168
- $emailTemplateVariables['specialprice'] = number_format($args['row']['product_special_price'],2);
169
-
170
- // Assign the values to the array of recipients
171
- $this->_saleRecipients[$args['row']['customer_email']]['cartId'] = $args['row']['cart_id'];
172
-
173
- // Get product image via collection
174
- $_productCollection = Mage::getResourceModel('catalog/product_collection');
175
- // Add attributes to the collection
176
- $_productCollection->addAttributeToFilter('entity_id',array('eq' => $args['row']['product_id']));
177
- // Add image to the collection
178
- $_productCollection->joinAttribute('image', 'catalog_product/image', 'entity_id', null, 'left');
179
- // Limit the collection to get the specific product
180
- $_productCollection->setPageSize(1);
181
-
182
- try {
183
- $productImg = (string)Mage::helper('catalog/image')->init($_productCollection->getFirstItem(), 'image');
184
- }
185
- catch (Exception $e) {
186
- $productImg = false;
187
- }
188
-
189
- // Add product image
190
- $emailTemplateVariables['productimage'] = $productImg;
191
- }
192
- else
193
- {
194
- // We create some extra variables if there is several products in the cart
195
- $emailTemplateVariables = $this->_saleRecipients[$args['row']['customer_email']]['emailTemplateVariables'];
196
- // Discount amount
197
- // If one product before
198
- if (!array_key_exists('discount',$emailTemplateVariables))
199
- {
200
- $emailTemplateVariables['discount'] = $emailTemplateVariables['cartprice'] - $emailTemplateVariables['specialprice'];
201
- }
202
- // We add the discount on the second product
203
- $moreDiscount = number_format($args['row']['product_price_in_cart'],2) - number_format($args['row']['product_special_price'],2);
204
- $emailTemplateVariables['discount'] += $moreDiscount;
205
- // We increase the product count
206
- if (!array_key_exists('extraproductcount',$emailTemplateVariables))
207
- {
208
- $emailTemplateVariables['extraproductcount'] = 0;
209
- }
210
- $emailTemplateVariables['extraproductcount'] += 1;
211
- }
212
-
213
- // Add currency codes to prices
214
- $emailTemplateVariables['cartprice'] = Mage::helper('core')->currency($emailTemplateVariables['cartprice'], true, false);
215
- $emailTemplateVariables['specialprice'] = Mage::helper('core')->currency($emailTemplateVariables['specialprice'], true, false);
216
- if (array_key_exists('discount',$emailTemplateVariables))
217
- {
218
- $emailTemplateVariables['discount'] = Mage::helper('core')->currency($emailTemplateVariables['discount'], true, false);
219
- }
220
-
221
- // Assign the array of template variables
222
- $this->_saleRecipients[$args['row']['customer_email']]['emailTemplateVariables'] = $emailTemplateVariables;
223
- $this->_saleRecipients[$args['row']['customer_email']]['store_id'] = $this->_currentStoreId;
224
- }
225
- }
226
-
227
- /**
228
- * @param $dryrun
229
- * @param $testemail
230
- */
231
- protected function _sendSaleEmails($dryrun,$testemail)
232
- {
233
- try
234
- {
235
- // Send the emails via a loop
236
- foreach ($this->_getSaleRecipients() as $email => $recipient)
237
- {
238
- // Store Id
239
- Mage::app()->setCurrentStore($recipient['store_id']);
240
- // Get the transactional email template
241
- $templateId = Mage::getStoreConfig('abandonedcartsconfig/options/email_template_sale');
242
- // Get the sender
243
- $sender = array();
244
- $sender['email'] = Mage::getStoreConfig('abandonedcartsconfig/options/email');
245
- $sender['name'] = Mage::getStoreConfig('abandonedcartsconfig/options/name');
246
-
247
- // Don't send the email if dryrun is set
248
- if ($dryrun)
249
- {
250
- // Log data when dried run
251
- Mage::helper('abandonedcarts')->log(__METHOD__);
252
- Mage::helper('abandonedcarts')->log($recipient['emailTemplateVariables']);
253
- // If the test email is set and found
254
- if (isset($testemail) && $email == $testemail)
255
- {
256
- Mage::helper('abandonedcarts')->log(__METHOD__ . "sendAbandonedCartsSaleEmail test: " . $email);
257
- // Send the test email
258
- Mage::getModel('core/email_template')
259
- ->sendTransactional(
260
- $templateId,
261
- $sender,
262
- $email,
263
- $recipient['emailTemplateVariables']['fullname'] ,
264
- $recipient['emailTemplateVariables'],
265
- null);
266
- }
267
- }
268
- else
269
- {
270
- Mage::helper('abandonedcarts')->log(__METHOD__ . "sendAbandonedCartsSaleEmail: " . $email);
271
-
272
- // Send the email
273
- Mage::getModel('core/email_template')
274
- ->sendTransactional(
275
- $templateId,
276
- $sender,
277
- $email,
278
- $recipient['emailTemplateVariables']['fullname'] ,
279
- $recipient['emailTemplateVariables'],
280
- null);
281
- }
282
-
283
- // Load the quote
284
- $quote = Mage::getModel('sales/quote')->load($recipient['cartId']);
285
-
286
- // We change the notification attribute
287
- $quote->setAbandonedSaleNotified(1);
288
-
289
- // Save only if dryrun is false or if the test email is set and found
290
- if (!$dryrun || (isset($testemail) && $email == $testemail))
291
- {
292
- $quote->getResource()->saveAttribute($quote,array('abandoned_sale_notified'));
293
- }
294
- }
295
- }
296
- catch (Exception $e)
297
- {
298
- Mage::helper('abandonedcarts')->log(__METHOD__ . " " . $e->getMessage());
299
- }
300
- }
301
-
302
- /**
303
- * @param $dryrun
304
- * @param $testemail
305
- */
306
- protected function _sendEmails($dryrun,$testemail)
307
- {
308
- try
309
- {
310
- // Send the emails via a loop
311
- foreach ($this->_getRecipients() as $email => $recipient)
312
- {
313
- // Store Id
314
- Mage::app()->setCurrentStore($recipient['store_id']);
315
- // Get the transactional email template
316
- $templateId = Mage::getStoreConfig('abandonedcartsconfig/options/email_template');
317
- // Get the sender
318
- $sender = array();
319
- $sender['email'] = Mage::getStoreConfig('abandonedcartsconfig/options/email');
320
- $sender['name'] = Mage::getStoreConfig('abandonedcartsconfig/options/name');
321
-
322
- // Don't send the email if dryrun is set
323
- if ($dryrun)
324
- {
325
- // Log data when dried run
326
- Mage::helper('abandonedcarts')->log(__METHOD__);
327
- Mage::helper('abandonedcarts')->log($recipient['emailTemplateVariables']);
328
- // If the test email is set and found
329
- if (isset($testemail) && $email == $testemail)
330
- {
331
- Mage::helper('abandonedcarts')->log(__METHOD__ . "sendAbandonedCartsEmail test: " . $email);
332
- // Send the test email
333
- Mage::getModel('core/email_template')
334
- ->sendTransactional(
335
- $templateId,
336
- $sender,
337
- $email,
338
- $recipient['emailTemplateVariables']['fullname'] ,
339
- $recipient['emailTemplateVariables'],
340
- null);
341
- }
342
- }
343
- else
344
- {
345
- Mage::helper('abandonedcarts')->log(__METHOD__ . "sendAbandonedCartsEmail: " . $email);
346
-
347
- // Send the email
348
- Mage::getModel('core/email_template')
349
- ->sendTransactional(
350
- $templateId,
351
- $sender,
352
- $email,
353
- $recipient['emailTemplateVariables']['fullname'] ,
354
- $recipient['emailTemplateVariables'],
355
- null);
356
- }
357
-
358
- // Load the quote
359
- $quote = Mage::getModel('sales/quote')->load($recipient['cartId']);
360
-
361
- // We change the notification attribute
362
- $quote->setAbandonedNotified(1);
363
-
364
- // Save only if dryrun is false or if the test email is set and found
365
- if (!$dryrun || (isset($testemail) && $email == $testemail))
366
- {
367
- $quote->getResource()->saveAttribute($quote,array('abandoned_notified'));
368
- }
369
- }
370
- }
371
- catch (Exception $e)
372
- {
373
- Mage::helper('abandonedcarts')->log(__METHOD__ . " " . $e->getMessage());
374
- }
375
- }
376
-
377
- /**
378
- * Send notification email to customer with abandoned cart containing sale products
379
- * @param boolean $dryrun if dryrun is set to true, it won't send emails and won't alter quotes
380
- * @param string $testemail email to test
381
- */
382
- public function sendAbandonedCartsSaleEmail($dryrun = false, $testemail = null)
383
- {
384
- try
385
- {
386
- if (Mage::helper('abandonedcarts')->getDryRun()) $dryrun = true;
387
- if (Mage::helper('abandonedcarts')->getTestEmail()) $testemail = Mage::helper('abandonedcarts')->getTestEmail();
388
- // Set customer groups
389
- $this->_customerGroups = $this->_customerGroups ? $this->_customerGroups : Mage::helper('abandonedcarts')->getCustomerGroupsLimitation();
390
- // Original store id
391
- $this->_originalStoreId = Mage::app()->getStore()->getId();
392
-
393
- if (Mage::helper('abandonedcarts')->isSaleEnabled())
394
- {
395
- $this->_setToday();
396
-
397
- // Get the attribute id for the status attribute
398
- $eavAttribute = Mage::getModel('eav/entity_attribute');
399
- $statusId = $eavAttribute->getIdByCode('catalog_product', 'status');
400
- $nameId = $eavAttribute->getIdByCode('catalog_product', 'name');
401
- $priceId = $eavAttribute->getIdByCode('catalog_product', 'price');
402
- $spriceId = $eavAttribute->getIdByCode('catalog_product', 'special_price');
403
- $spfromId = $eavAttribute->getIdByCode('catalog_product', 'special_from_date');
404
- $sptoId = $eavAttribute->getIdByCode('catalog_product', 'special_to_date');
405
-
406
- // Loop through the stores
407
- foreach (Mage::app()->getWebsites() as $website) {
408
- // Get the website id
409
- $websiteId = $website->getWebsiteId();
410
- foreach ($website->getGroups() as $group) {
411
- $stores = $group->getStores();
412
- foreach ($stores as $store) {
413
-
414
- // Get the store id
415
- $storeId = $store->getStoreId();
416
- $this->_currentStoreId = $storeId;
417
-
418
- // Init the store to be able to load the quote and the collections properly
419
- Mage::app()->init($storeId,'store');
420
-
421
- // Get the product collection
422
- $collection = Mage::getResourceModel('catalog/product_collection')->setStore($storeId);
423
-
424
- // Database TableNams
425
- $eavEntityType = Mage::getSingleton("core/resource")->getTableName('eav_entity_type');
426
- $eavAttribute = Mage::getSingleton("core/resource")->getTableName('eav_attribute');
427
-
428
- // If flat catalog is enabled
429
- if (Mage::helper('catalog/product_flat')->isEnabled())
430
- {
431
- // First collection: carts with products that became on sale
432
- // Join the collection with the required tables
433
- $collection->getSelect()
434
- ->reset(Zend_Db_Select::COLUMNS)
435
- ->columns(array('e.entity_id AS product_id',
436
- 'e.sku',
437
- 'catalog_flat.name as product_name',
438
- 'catalog_flat.price as product_price',
439
- 'catalog_flat.special_price as product_special_price',
440
- 'catalog_flat.special_from_date as product_special_from_date',
441
- 'catalog_flat.special_to_date as product_special_to_date',
442
- 'quote_table.entity_id as cart_id',
443
- 'quote_table.updated_at as cart_updated_at',
444
- 'quote_table.abandoned_sale_notified as has_been_notified',
445
- 'quote_items.price as product_price_in_cart',
446
- 'quote_table.customer_email as customer_email',
447
- 'quote_table.customer_firstname as customer_firstname',
448
- 'quote_table.customer_lastname as customer_lastname',
449
- 'quote_table.customer_group_id as customer_group'
450
- )
451
- )
452
- ->joinInner(
453
- array('quote_items' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote_item')),
454
- 'quote_items.product_id = e.entity_id AND quote_items.price > 0.00',
455
- null)
456
- ->joinInner(
457
- array('quote_table' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote')),
458
- 'quote_items.quote_id = quote_table.entity_id AND quote_table.items_count > 0 AND quote_table.is_active = 1 AND quote_table.customer_email IS NOT NULL AND quote_table.abandoned_sale_notified = 0 AND quote_table.store_id = '.$storeId,
459
- null)
460
- ->joinInner(
461
- array('catalog_flat' => Mage::getSingleton("core/resource")->getTableName('catalog_product_flat_'.$storeId)),
462
- 'catalog_flat.entity_id = e.entity_id',
463
- null)
464
- ->joinInner(
465
- array('catalog_enabled' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_int')),
466
- 'catalog_enabled.entity_id = e.entity_id AND catalog_enabled.attribute_id = '.$statusId.' AND catalog_enabled.value = 1',
467
- null)
468
- ->joinInner(
469
- array('inventory' => Mage::getSingleton("core/resource")->getTableName('cataloginventory_stock_status')),
470
- 'inventory.product_id = e.entity_id AND inventory.stock_status = 1 AND inventory.website_id = '.$websiteId,
471
- null)
472
- ->order('quote_table.updated_at DESC');
473
- }
474
- else
475
- {
476
- // First collection: carts with products that became on sale
477
- // Join the collection with the required tables
478
- $collection->getSelect()
479
- ->reset(Zend_Db_Select::COLUMNS)
480
- ->columns(array('e.entity_id AS product_id',
481
- 'e.sku',
482
- 'catalog_name.value as product_name',
483
- 'catalog_price.value as product_price',
484
- 'catalog_sprice.value as product_special_price',
485
- 'catalog_spfrom.value as product_special_from_date',
486
- 'catalog_spto.value as product_special_to_date',
487
- 'quote_table.entity_id as cart_id',
488
- 'quote_table.updated_at as cart_updated_at',
489
- 'quote_table.abandoned_sale_notified as has_been_notified',
490
- 'quote_items.price as product_price_in_cart',
491
- 'quote_table.customer_email as customer_email',
492
- 'quote_table.customer_firstname as customer_firstname',
493
- 'quote_table.customer_lastname as customer_lastname',
494
- 'quote_table.customer_group_id as customer_group'
495
- )
496
- )
497
- // Name
498
- ->joinInner(
499
- array('catalog_name' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_varchar')),
500
- "catalog_name.entity_id = e.entity_id AND catalog_name.attribute_id = $nameId",
501
- null)
502
- // Price
503
- ->joinInner(
504
- array('catalog_price' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_decimal')),
505
- "catalog_price.entity_id = e.entity_id AND catalog_price.attribute_id = $priceId",
506
- null)
507
- // Special Price
508
- ->joinInner(
509
- array('catalog_sprice' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_decimal')),
510
- "catalog_sprice.entity_id = e.entity_id AND catalog_sprice.attribute_id = $spriceId",
511
- null)
512
- // Special From Date
513
- ->joinInner(
514
- array('catalog_spfrom' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_datetime')),
515
- "catalog_spfrom.entity_id = e.entity_id AND catalog_spfrom.attribute_id = $spfromId",
516
- null)
517
- // Special To Date
518
- ->joinInner(
519
- array('catalog_spto' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_datetime')),
520
- "catalog_spto.entity_id = e.entity_id AND catalog_spto.attribute_id = $sptoId",
521
- null)
522
- ->joinInner(
523
- array('quote_items' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote_item')),
524
- 'quote_items.product_id = e.entity_id AND quote_items.price > 0.00',
525
- null)
526
- ->joinInner(
527
- array('quote_table' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote')),
528
- 'quote_items.quote_id = quote_table.entity_id AND quote_table.items_count > 0 AND quote_table.is_active = 1 AND quote_table.customer_email IS NOT NULL AND quote_table.abandoned_sale_notified = 0 AND quote_table.store_id = '.$storeId,
529
- null)
530
- ->joinInner(
531
- array('catalog_enabled' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_int')),
532
- 'catalog_enabled.entity_id = e.entity_id AND catalog_enabled.attribute_id = '.$statusId.' AND catalog_enabled.value = 1',
533
- null)
534
- ->joinInner(
535
- array('inventory' => Mage::getSingleton("core/resource")->getTableName('cataloginventory_stock_status')),
536
- 'inventory.product_id = e.entity_id AND inventory.stock_status = 1 AND inventory.website_id = '.$websiteId,
537
- null)
538
- ->order('quote_table.updated_at DESC');
539
- }
540
-
541
- //$collection->printlogquery(true,true);
542
- $collection->load();
543
-
544
- // Skip the rest of the code if the collection is empty
545
- if ($collection->getSize() == 0) continue;
546
-
547
- // Call iterator walk method with collection query string and callback method as parameters
548
- // Has to be used to handle massive collection instead of foreach
549
- Mage::getSingleton('core/resource_iterator')->walk($collection->getSelect(), array(array($this, 'generateSaleRecipients')));
550
- }
551
- }
552
- }
553
-
554
- // Send the emails
555
- $this->_sendSaleEmails($dryrun,$testemail);
556
- }
557
-
558
- Mage::app()->setCurrentStore($this->_originalStoreId);
559
- }
560
- catch (Exception $e)
561
- {
562
- Mage::app()->setCurrentStore($this->_originalStoreId);
563
- Mage::helper('abandonedcarts')->log(__METHOD__ . " " . $e->getMessage());
564
- }
565
- }
566
-
567
- /**
568
- * Send notification email to customer with abandoned carts after the number of days specified in the config
569
- * @param bool $nodate
570
- * @param boolean $dryrun if dryrun is set to true, it won't send emails and won't alter quotes
571
- * @param string $testemail email to test
572
- */
573
- public function sendAbandonedCartsEmail($nodate = false, $dryrun = false, $testemail = null)
574
- {
575
- if (Mage::helper('abandonedcarts')->getDryRun()) $dryrun = true;
576
- if (Mage::helper('abandonedcarts')->getTestEmail()) $testemail = Mage::helper('abandonedcarts')->getTestEmail();
577
- // Set customer groups
578
- $this->_customerGroups = $this->_customerGroups ? $this->_customerGroups : Mage::helper('abandonedcarts')->getCustomerGroupsLimitation();
579
-
580
- $this->_originalStoreId = Mage::app()->getStore()->getId();
581
-
582
- try
583
- {
584
- if (Mage::helper('abandonedcarts')->isEnabled())
585
- {
586
- // Date handling
587
- $store = Mage_Core_Model_App::ADMIN_STORE_ID;
588
- $timezone = Mage::app()->getStore($store)->getConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE);
589
- date_default_timezone_set($timezone);
590
-
591
- // If the nodate parameter is set to false
592
- if (!$nodate)
593
- {
594
- // Get the delay provided and convert it to a proper date
595
- $delay = Mage::getStoreConfig('abandonedcartsconfig/options/notify_delay');
596
- $delay = date('Y-m-d H:i:s', time() - $delay * 24 * 3600);
597
- }
598
- else
599
- {
600
- // We create a date in the future to handle all abandoned carts
601
- $delay = date('Y-m-d H:i:s', strtotime("+7 day"));
602
- }
603
-
604
- // Get the attribute id for several attributes
605
- $eavAttribute = Mage::getModel('eav/entity_attribute');
606
- $statusId = $eavAttribute->getIdByCode('catalog_product', 'status');
607
- $nameId = $eavAttribute->getIdByCode('catalog_product', 'name');
608
- $priceId = $eavAttribute->getIdByCode('catalog_product', 'price');
609
-
610
- // Loop through the stores
611
- foreach (Mage::app()->getWebsites() as $website) {
612
- // Get the website id
613
- $websiteId = $website->getWebsiteId();
614
- foreach ($website->getGroups() as $group) {
615
- $stores = $group->getStores();
616
- foreach ($stores as $store) {
617
-
618
- // Get the store id
619
- $storeId = $store->getStoreId();
620
- $this->_currentStoreId = $storeId;
621
- // Init the store to be able to load the quote and the collections properly
622
- Mage::app()->init($storeId,'store');
623
-
624
- // Get the product collection
625
- $collection = Mage::getResourceModel('catalog/product_collection')->setStore($storeId);
626
-
627
- // If flat catalog is enabled
628
- if (Mage::helper('catalog/product_flat')->isEnabled())
629
- {
630
- // First collection: carts with products that became on sale
631
- // Join the collection with the required tables
632
- $collection->getSelect()
633
- ->reset(Zend_Db_Select::COLUMNS)
634
- ->columns(array('e.entity_id AS product_id',
635
- 'e.sku',
636
- 'catalog_flat.name as product_name',
637
- 'catalog_flat.price as product_price',
638
- 'quote_table.entity_id as cart_id',
639
- 'quote_table.updated_at as cart_updated_at',
640
- 'quote_table.abandoned_notified as has_been_notified',
641
- 'quote_table.customer_email as customer_email',
642
- 'quote_table.customer_firstname as customer_firstname',
643
- 'quote_table.customer_lastname as customer_lastname',
644
- 'quote_table.customer_group_id as customer_group'
645
- )
646
- )
647
- ->joinInner(
648
- array('quote_items' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote_item')),
649
- 'quote_items.product_id = e.entity_id AND quote_items.price > 0.00',
650
- null)
651
- ->joinInner(
652
- array('quote_table' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote')),
653
- 'quote_items.quote_id = quote_table.entity_id AND quote_table.items_count > 0 AND quote_table.is_active = 1 AND quote_table.customer_email IS NOT NULL AND quote_table.abandoned_notified = 0 AND quote_table.updated_at < "'.$delay.'" AND quote_table.store_id = '.$storeId,
654
- null)
655
- ->joinInner(
656
- array('catalog_flat' => Mage::getSingleton("core/resource")->getTableName('catalog_product_flat_'.$storeId)),
657
- 'catalog_flat.entity_id = e.entity_id',
658
- null)
659
- ->joinInner(
660
- array('catalog_enabled' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_int')),
661
- 'catalog_enabled.entity_id = e.entity_id AND catalog_enabled.attribute_id = '.$statusId.' AND catalog_enabled.value = 1',
662
- null)
663
- ->joinInner(
664
- array('inventory' => Mage::getSingleton("core/resource")->getTableName('cataloginventory_stock_status')),
665
- 'inventory.product_id = e.entity_id AND inventory.stock_status = 1 AND website_id = '.$websiteId,
666
- null)
667
- ->order('quote_table.updated_at DESC');
668
- }
669
- else
670
- {
671
- // First collection: carts with products that became on sale
672
- // Join the collection with the required tables
673
- $collection->getSelect()
674
- ->reset(Zend_Db_Select::COLUMNS)
675
- ->columns(array('e.entity_id AS product_id',
676
- 'e.sku',
677
- 'catalog_name.value as product_name',
678
- 'catalog_price.value as product_price',
679
- 'quote_table.entity_id as cart_id',
680
- 'quote_table.updated_at as cart_updated_at',
681
- 'quote_table.abandoned_notified as has_been_notified',
682
- 'quote_table.customer_email as customer_email',
683
- 'quote_table.customer_firstname as customer_firstname',
684
- 'quote_table.customer_lastname as customer_lastname',
685
- 'quote_table.customer_group_id as customer_group'
686
- )
687
- )
688
- // Name
689
- ->joinInner(
690
- array('catalog_name' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_varchar')),
691
- "catalog_name.entity_id = e.entity_id AND catalog_name.attribute_id = $nameId",
692
- null)
693
- // Price
694
- ->joinInner(
695
- array('catalog_price' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_decimal')),
696
- "catalog_price.entity_id = e.entity_id AND catalog_price.attribute_id = $priceId",
697
- null)
698
- ->joinInner(
699
- array('quote_items' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote_item')),
700
- 'quote_items.product_id = e.entity_id AND quote_items.price > 0.00',
701
- null)
702
- ->joinInner(
703
- array('quote_table' => Mage::getSingleton("core/resource")->getTableName('sales_flat_quote')),
704
- 'quote_items.quote_id = quote_table.entity_id AND quote_table.items_count > 0 AND quote_table.is_active = 1 AND quote_table.customer_email IS NOT NULL AND quote_table.abandoned_notified = 0 AND quote_table.updated_at < "'.$delay.'" AND quote_table.store_id = '.$storeId,
705
- null)
706
- ->joinInner(
707
- array('catalog_enabled' => Mage::getSingleton("core/resource")->getTableName('catalog_product_entity_int')),
708
- 'catalog_enabled.entity_id = e.entity_id AND catalog_enabled.attribute_id = '.$statusId.' AND catalog_enabled.value = 1',
709
- null)
710
- ->joinInner(
711
- array('inventory' => Mage::getSingleton("core/resource")->getTableName('cataloginventory_stock_status')),
712
- 'inventory.product_id = e.entity_id AND inventory.stock_status = 1 AND website_id = '.$websiteId,
713
- null)
714
- ->order('quote_table.updated_at DESC');
715
- }
716
-
717
- //$collection->printlogquery(true,true);
718
- $collection->load();
719
-
720
- // Skip the rest of the code if the collection is empty
721
- if ($collection->getSize() == 0) continue;
722
-
723
- // Call iterator walk method with collection query string and callback method as parameters
724
- // Has to be used to handle massive collection instead of foreach
725
- Mage::getSingleton('core/resource_iterator')->walk($collection->getSelect(), array(array($this, 'generateRecipients')));
726
- }
727
- }
728
- }
729
-
730
- // Send the emails
731
- $this->_sendEmails($dryrun,$testemail);
732
- }
733
-
734
- Mage::app()->setCurrentStore($this->_originalStoreId);
735
- }
736
- catch (Exception $e)
737
- {
738
- Mage::app()->setCurrentStore($this->_originalStoreId);
739
- Mage::helper('abandonedcarts')->log(__METHOD__ . " " . $e->getMessage());
740
- }
741
- }
742
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/code/community/DigitalPianism/Abandonedcarts/Model/Resource/Link.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Model_Resource_Link
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Model_Resource_Link extends Mage_Core_Model_Mysql4_Abstract
7
+ {
8
+
9
+ protected function _construct()
10
+ {
11
+ $this->_init('abandonedcarts/link', 'link_id');
12
+ }
13
+
14
+ }
app/code/community/DigitalPianism/Abandonedcarts/Model/Resource/Link/Collection.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Model_Resource_Link_Collection
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Model_Resource_Link_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract {
7
+
8
+ public function _construct()
9
+ {
10
+ parent::_construct();
11
+ $this->_init('abandonedcarts/link');
12
+ }
13
+
14
+ }
app/code/community/DigitalPianism/Abandonedcarts/Model/Resource/Log.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Model_Resource_Log
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Model_Resource_Log extends Mage_Core_Model_Mysql4_Abstract
7
+ {
8
+
9
+ protected function _construct()
10
+ {
11
+ $this->_init('abandonedcarts/log', 'log_id');
12
+ }
13
+
14
+ }
app/code/community/DigitalPianism/Abandonedcarts/Model/Resource/Log/Collection.php ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_Model_Resource_Log_Collection
5
+ */
6
+ class DigitalPianism_Abandonedcarts_Model_Resource_Log_Collection extends Mage_Core_Model_Mysql4_Collection_Abstract {
7
+
8
+ public function _construct()
9
+ {
10
+ parent::_construct();
11
+ $this->_init('abandonedcarts/log');
12
+ }
13
+
14
+ }
app/code/community/DigitalPianism/Abandonedcarts/controllers/Adminhtml/AbandonedcartsController.php CHANGED
@@ -12,21 +12,123 @@ class DigitalPianism_Abandonedcarts_Adminhtml_AbandonedcartsController extends M
12
  */
13
  protected function _isAllowed()
14
  {
15
- return Mage::getSingleton('admin/session')->isAllowed('system/config/abandonedcartsconfig');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  }
17
 
18
  /**
19
- * Manually send the notifications
20
  *
21
- * @return void
22
  */
23
- public function sendAction()
24
- {
25
- $model = Mage::getModel('abandonedcarts/observer');
26
- $model->sendAbandonedCartsEmail(true);
27
- $model->sendAbandonedCartsSaleEmail();
28
-
29
- $result = 1;
30
- Mage::app()->getResponse()->setBody($result);
 
 
 
 
 
 
 
 
 
 
31
  }
32
  }
12
  */
13
  protected function _isAllowed()
14
  {
15
+ return Mage::getSingleton('admin/session')->isAllowed('digitalpianism_menu/abandonedcarts');
16
+ }
17
+
18
+ protected function _initAction()
19
+ {
20
+ $this->loadLayout()
21
+ ->_setActiveMenu('digitalpianism_menu/abandonedcarts');
22
+
23
+ return $this;
24
+ }
25
+
26
+ public function logsAction()
27
+ {
28
+ $this->_initAction();
29
+ $this->_addContent($this->getLayout()->createBlock('abandonedcarts/adminhtml_logs'));
30
+ $this->renderLayout();
31
+ }
32
+
33
+ public function gridAction()
34
+ {
35
+ $this->loadLayout();
36
+ $this->renderLayout();
37
+ }
38
+
39
+ public function indexAction()
40
+ {
41
+ $this->_initAction();
42
+ $this->_addContent($this->getLayout()->createBlock('abandonedcarts/adminhtml_abandonedcarts'));
43
+ $this->renderLayout();
44
+ }
45
+
46
+ public function salegridAction()
47
+ {
48
+ $this->loadLayout();
49
+ $this->renderLayout();
50
+ }
51
+
52
+ public function saleAction()
53
+ {
54
+ $this->_initAction();
55
+ $this->_addContent($this->getLayout()->createBlock('abandonedcarts/adminhtml_saleabandonedcarts'));
56
+ $this->renderLayout();
57
+ }
58
+
59
+ public function notifyAllAction()
60
+ {
61
+ try {
62
+ $count = Mage::getModel('abandonedcarts/notifier')->sendAbandonedCartsEmail();
63
+ Mage::getSingleton('adminhtml/session')->addSuccess(
64
+ Mage::helper('abandonedcarts')->__(
65
+ '%sTotal of %d customer(s) were successfully notified', (Mage::helper('abandonedcarts')->getDryRun() ? "!DRY RUN! " : ""), $count
66
+ )
67
+ );
68
+ } catch (Exception $e) {
69
+ Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
70
+ }
71
+ $this->_redirect('*/*/index');
72
+ }
73
+
74
+ public function notifySaleAllAction()
75
+ {
76
+ try {
77
+ $count = Mage::getModel('abandonedcarts/notifier')->sendAbandonedCartsSaleEmail();
78
+ Mage::getSingleton('adminhtml/session')->addSuccess(
79
+ Mage::helper('abandonedcarts')->__(
80
+ '%sTotal of %d customer(s) were successfully notified', (Mage::helper('abandonedcarts')->getDryRun() ? "!DRY RUN! " : ""), $count
81
+ )
82
+ );
83
+ } catch (Exception $e) {
84
+ Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
85
+ }
86
+ $this->_redirect('*/*/sale');
87
+ }
88
+
89
+ /**
90
+ *
91
+ */
92
+ public function notifySaleAction()
93
+ {
94
+ $emails = $this->getRequest()->getParam('abandonedcarts');
95
+ if (!is_array($emails)) {
96
+ Mage::getSingleton('adminhtml/session')->addError(Mage::helper('abandonedcarts')->__('Please select email(s)'));
97
+ } else {
98
+ try {
99
+ Mage::getModel('abandonedcarts/notifier')->sendAbandonedCartsSaleEmail(false, false, $emails);
100
+ Mage::getSingleton('adminhtml/session')->addSuccess(
101
+ Mage::helper('abandonedcarts')->__(
102
+ '%sTotal of %d customer(s) were successfully notified', (Mage::helper('abandonedcarts')->getDryRun() ? "!DRY RUN! " : ""), count($emails)
103
+ )
104
+ );
105
+ } catch (Exception $e) {
106
+ Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
107
+ }
108
+ }
109
+ $this->_redirect('*/*/sale');
110
  }
111
 
112
  /**
 
113
  *
 
114
  */
115
+ public function notifyAction()
116
+ {
117
+ $emails = $this->getRequest()->getParam('abandonedcarts');
118
+ if (!is_array($emails)) {
119
+ Mage::getSingleton('adminhtml/session')->addError(Mage::helper('abandonedcarts')->__('Please select email(s)'));
120
+ } else {
121
+ try {
122
+ Mage::getModel('abandonedcarts/notifier')->sendAbandonedCartsEmail(false, false, null, $emails);
123
+ Mage::getSingleton('adminhtml/session')->addSuccess(
124
+ Mage::helper('abandonedcarts')->__(
125
+ '%sTotal of %d customer(s) were successfully notified', (Mage::helper('abandonedcarts')->getDryRun() ? "!DRY RUN! " : ""), count($emails)
126
+ )
127
+ );
128
+ } catch (Exception $e) {
129
+ Mage::getSingleton('adminhtml/session')->addError($e->getMessage());
130
+ }
131
+ }
132
+ $this->_redirect('*/*/index');
133
  }
134
  }
app/code/community/DigitalPianism/Abandonedcarts/controllers/IndexController.php ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class DigitalPianism_Abandonedcarts_IndexController
5
+ */
6
+ class DigitalPianism_Abandonedcarts_IndexController extends Mage_Core_Controller_Front_Action {
7
+
8
+ /**
9
+ *
10
+ */
11
+ public function indexAction()
12
+ {
13
+ // Get the token
14
+ if ($token = $this->getRequest()->getParam('token')) {
15
+
16
+ // Find corresponding hash entry in the database
17
+ $link = Mage::getResourceModel('abandonedcarts/link_collection')
18
+ ->addFieldToSelect(array('link_id','customer_email'))
19
+ ->addFieldToFilter('token_hash', hash('sha256', $token))
20
+ ->setPageSize(1);
21
+
22
+ if ($link->getSize()) {
23
+
24
+ $customerEmail = $link->getFirstItem()->getCustomerEmail();
25
+ /**
26
+ * @TODO add an entry in the log
27
+ */
28
+ // Delete so it's one use only
29
+ $link->getFirstItem()->delete();
30
+
31
+ /** @var Mage_Customer_Model_Customer $customer */
32
+ $customer = Mage::getModel('customer/customer')
33
+ ->setWebsiteId(Mage::app()->getStore()->getWebsiteId());
34
+
35
+ // Load the customer
36
+ $customer->loadByEmail($customerEmail);
37
+
38
+ // Log the customer in
39
+ if ($customer->getId()) {
40
+ Mage::getSingleton('customer/session')->setCustomerAsLoggedIn($customer);
41
+ Mage::getSingleton('customer/session')->renewSession();
42
+ // Redirects to cart
43
+ $this->_redirect('checkout/cart');
44
+ }
45
+ }
46
+ }
47
+
48
+ $this->_redirect('checkout/cart');
49
+ }
50
+ }
app/code/community/DigitalPianism/Abandonedcarts/data/abandonedcarts_setup/data-upgrade-1.0.0-1.0.1.php ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $installer = $this;
4
+ $installer->startSetup();
5
+
6
+ /** @var Mage_Core_Model_Config $coreConfig */
7
+ $coreConfig = Mage::getModel('core/config');
8
+
9
+ if ($data = Mage::getStoreConfig('abandonedcartsconfig/options/name')) {
10
+ $coreConfig->saveConfig('abandonedcartsconfig/email/name', $data);
11
+ }
12
+
13
+ if ($data = Mage::getStoreConfig('abandonedcartsconfig/options/email')) {
14
+ $coreConfig->saveConfig('abandonedcartsconfig/email/email', $data);
15
+ }
16
+
17
+ if ($data = Mage::getStoreConfig('abandonedcartsconfig/options/email_template')) {
18
+ $coreConfig->saveConfig('abandonedcartsconfig/email/email_template', $data);
19
+ }
20
+
21
+ if ($data = Mage::getStoreConfig('abandonedcartsconfig/options/email_template_sale')) {
22
+ $coreConfig->saveConfig('abandonedcartsconfig/email/email_template_sale', $data);
23
+ }
24
+
25
+ if ($data = Mage::getStoreConfig('abandonedcartsconfig/options/dryrun')) {
26
+ $coreConfig->saveConfig('abandonedcartsconfig/test/dryrun', $data);
27
+ }
28
+
29
+ if ($data = Mage::getStoreConfig('abandonedcartsconfig/options/testemail')) {
30
+ $coreConfig->saveConfig('abandonedcartsconfig/test/testemail', $data);
31
+ }
32
+
33
+ $installer->endSetup();
app/code/community/DigitalPianism/Abandonedcarts/etc/adminhtml.xml CHANGED
@@ -1,5 +1,39 @@
1
  <?xml version="1.0"?>
2
  <config>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  <acl>
4
  <resources>
5
  <all>
@@ -7,6 +41,32 @@
7
  </all>
8
  <admin>
9
  <children>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  <system>
11
  <children>
12
  <config>
1
  <?xml version="1.0"?>
2
  <config>
3
+ <menu>
4
+ <digitalpianism_menu translate="title">
5
+ <title>Digital Pianism</title>
6
+ <sort_order>110</sort_order>
7
+ <children>
8
+ <abandonedcarts translate="title" module="abandonedcarts">
9
+ <title>Abandoned Carts</title>
10
+ <sort_order>1</sort_order>
11
+ <children>
12
+ <logs translate="title" module="abandonedcarts">
13
+ <title>Logs</title>
14
+ <sort_order>1</sort_order>
15
+ <action>adminhtml/abandonedcarts/logs</action>
16
+ </logs>
17
+ <list translate="title" module="abandonedcarts">
18
+ <title>Abandoned Carts List</title>
19
+ <sort_order>2</sort_order>
20
+ <action>adminhtml/abandonedcarts/index</action>
21
+ </list>
22
+ <sale_list translate="title" module="abandonedcarts">
23
+ <title>Abandoned Carts Sale List</title>
24
+ <sort_order>3</sort_order>
25
+ <action>adminhtml/abandonedcarts/sale</action>
26
+ </sale_list>
27
+ <config translate="title" module="abandonedcarts">
28
+ <title>Configuration</title>
29
+ <sort_order>4</sort_order>
30
+ <action>adminhtml/system_config/edit/section/abandonedcartsconfig</action>
31
+ </config>
32
+ </children>
33
+ </abandonedcarts>
34
+ </children>
35
+ </digitalpianism_menu>
36
+ </menu>
37
  <acl>
38
  <resources>
39
  <all>
41
  </all>
42
  <admin>
43
  <children>
44
+ <digitalpianism_menu>
45
+ <children>
46
+ <abandonedcarts translate="title">
47
+ <title>Abandoned Carts</title>
48
+ <sort_order>1</sort_order>
49
+ <children>
50
+ <logs translate="title">
51
+ <title>Logs</title>
52
+ <sort_order>1</sort_order>
53
+ </logs>
54
+ <list translate="title" module="abandonedcarts">
55
+ <title>Abandoned Carts List</title>
56
+ <sort_order>2</sort_order>
57
+ </list>
58
+ <sale_list translate="title" module="abandonedcarts">
59
+ <title>Abandoned Carts Sale List</title>
60
+ <sort_order>3</sort_order>
61
+ </sale_list>
62
+ <config translate="title">
63
+ <title>Configuration</title>
64
+ <sort_order>4</sort_order>
65
+ </config>
66
+ </children>
67
+ </abandonedcarts>
68
+ </children>
69
+ </digitalpianism_menu>
70
  <system>
71
  <children>
72
  <config>
app/code/community/DigitalPianism/Abandonedcarts/etc/config.xml CHANGED
@@ -4,7 +4,7 @@
4
 
5
  <modules>
6
  <DigitalPianism_Abandonedcarts>
7
- <version>0.3.6</version>
8
  </DigitalPianism_Abandonedcarts>
9
  </modules>
10
 
@@ -33,6 +33,20 @@
33
  </DigitalPianism_Abandonedcarts>
34
  </modules>
35
  </translate>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  </frontend>
37
 
38
  <adminhtml>
@@ -46,6 +60,46 @@
46
  </DigitalPianism_Abandonedcarts>
47
  </modules>
48
  </translate>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  </adminhtml>
50
 
51
  <global>
@@ -64,7 +118,22 @@
64
  <models>
65
  <abandonedcarts>
66
  <class>DigitalPianism_Abandonedcarts_Model</class>
 
67
  </abandonedcarts>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  <sales_resource>
69
  <rewrite>
70
  <quote>DigitalPianism_Abandonedcarts_Model_Sales_Resource_Quote</quote>
@@ -117,7 +186,7 @@
117
  <config_path>abandonedcartsconfig/options/cron_expr</config_path>
118
  </schedule>
119
  <run>
120
- <model>abandonedcarts/observer::sendAbandonedCartsEmail</model>
121
  </run>
122
  </digitalpianism_abandonedcarts_send>
123
  <digitalpianism_abandonedcarts_sendsale>
@@ -125,21 +194,41 @@
125
  <config_path>abandonedcartsconfig/options/cron_expr</config_path>
126
  </schedule>
127
  <run>
128
- <model>abandonedcarts/observer::sendAbandonedCartsSaleEmail</model>
129
  </run>
130
  </digitalpianism_abandonedcarts_sendsale>
 
 
 
 
 
 
 
 
131
  </jobs>
132
  </crontab>
133
 
134
  <default>
135
  <abandonedcartsconfig>
136
  <options>
 
137
  <cron_expr>0 1 * * *</cron_expr>
138
  <notify_delay>20</notify_delay>
139
- <email_template>abandonedcartsconfig_options_email_template</email_template>
140
- <email_template_sale>abandonedcartsconfig_options_email_template_sale</email_template_sale>
141
  <customer_groups>1,2,3</customer_groups>
142
  </options>
 
 
 
 
 
 
 
 
 
 
 
 
143
  </abandonedcartsconfig>
144
  </default>
145
 
4
 
5
  <modules>
6
  <DigitalPianism_Abandonedcarts>
7
+ <version>1.0.0</version>
8
  </DigitalPianism_Abandonedcarts>
9
  </modules>
10
 
33
  </DigitalPianism_Abandonedcarts>
34
  </modules>
35
  </translate>
36
+
37
+ <secure_url>
38
+ <abandonedcarts>/abandonedcarts/</abandonedcarts>
39
+ </secure_url>
40
+
41
+ <routers>
42
+ <abandonedcarts>
43
+ <use>standard</use>
44
+ <args>
45
+ <module>DigitalPianism_Abandonedcarts</module>
46
+ <frontName>abandonedcarts</frontName>
47
+ </args>
48
+ </abandonedcarts>
49
+ </routers>
50
  </frontend>
51
 
52
  <adminhtml>
60
  </DigitalPianism_Abandonedcarts>
61
  </modules>
62
  </translate>
63
+
64
+ <layout>
65
+ <updates>
66
+ <abandonedcarts module="DigitalPianism_Abandonedcarts">
67
+ <file>digitalpianism/abandonedcarts.xml</file>
68
+ </abandonedcarts>
69
+ </updates>
70
+ </layout>
71
+
72
+ <events>
73
+ <!-- To register the controller action -->
74
+ <controller_action_predispatch_adminhtml>
75
+ <observers>
76
+ <digitalpianism_abandonedcarts_predispatch_register>
77
+ <type>singleton</type>
78
+ <class>abandonedcarts/adminhtml_observer</class>
79
+ <method>registerController</method>
80
+ </digitalpianism_abandonedcarts_predispatch_register>
81
+ </observers>
82
+ </controller_action_predispatch_adminhtml>
83
+ <!-- Called after creating a block -->
84
+ <core_layout_block_create_after>
85
+ <observers>
86
+ <digitalpianism_abandonedcarts_block_create_after>
87
+ <type>singleton</type>
88
+ <class>abandonedcarts/adminhtml_observer</class>
89
+ <method>addExtraColumnsToGrid</method>
90
+ </digitalpianism_abandonedcarts_block_create_after>
91
+ </observers>
92
+ </core_layout_block_create_after>
93
+ <!-- Called before loading a non EAV collection -->
94
+ <core_collection_abstract_load_before>
95
+ <observers>
96
+ <digitalpianism_abandonedcarts_before_core_load_collection>
97
+ <class>abandonedcarts/adminhtml_observer</class>
98
+ <method>addExtraColumnsToCollection</method>
99
+ </digitalpianism_abandonedcarts_before_core_load_collection>
100
+ </observers>
101
+ </core_collection_abstract_load_before>
102
+ </events>
103
  </adminhtml>
104
 
105
  <global>
118
  <models>
119
  <abandonedcarts>
120
  <class>DigitalPianism_Abandonedcarts_Model</class>
121
+ <resourceModel>abandonedcarts_mysql4</resourceModel>
122
  </abandonedcarts>
123
+
124
+ <abandonedcarts_mysql4>
125
+ <class>DigitalPianism_Abandonedcarts_Model_Resource</class>
126
+ <!-- declare table test -->
127
+ <entities>
128
+ <log>
129
+ <table>dp_abandonedcarts_log</table>
130
+ </log>
131
+ <link>
132
+ <table>dp_abandonedcarts_link</table>
133
+ </link>
134
+ </entities>
135
+ </abandonedcarts_mysql4>
136
+
137
  <sales_resource>
138
  <rewrite>
139
  <quote>DigitalPianism_Abandonedcarts_Model_Sales_Resource_Quote</quote>
186
  <config_path>abandonedcartsconfig/options/cron_expr</config_path>
187
  </schedule>
188
  <run>
189
+ <model>abandonedcarts/notifier::sendAbandonedCartsEmail</model>
190
  </run>
191
  </digitalpianism_abandonedcarts_send>
192
  <digitalpianism_abandonedcarts_sendsale>
194
  <config_path>abandonedcartsconfig/options/cron_expr</config_path>
195
  </schedule>
196
  <run>
197
+ <model>abandonedcarts/notifier::sendAbandonedCartsSaleEmail</model>
198
  </run>
199
  </digitalpianism_abandonedcarts_sendsale>
200
+ <digitalpianism_abandonedcarts_deleteexpiredlinks>
201
+ <schedule>
202
+ <cron_expr>*/5 * * * *</cron_expr>
203
+ </schedule>
204
+ <run>
205
+ <model>abandonedcarts/link_cleaner::cleanExpiredLinks</model>
206
+ </run>
207
+ </digitalpianism_abandonedcarts_deleteexpiredlinks>
208
  </jobs>
209
  </crontab>
210
 
211
  <default>
212
  <abandonedcartsconfig>
213
  <options>
214
+ <enable>0</enable>
215
  <cron_expr>0 1 * * *</cron_expr>
216
  <notify_delay>20</notify_delay>
217
+ <enable_sale>1</enable_sale>
 
218
  <customer_groups>1,2,3</customer_groups>
219
  </options>
220
+ <email>
221
+ <email_template>abandonedcartsconfig_options_email_template</email_template>
222
+ <email_template_sale>abandonedcartsconfig_options_email_template_sale</email_template_sale>
223
+ <autologin>0</autologin>
224
+ </email>
225
+ <campaign>
226
+ <enable>0</enable>
227
+ </campaign>
228
+ <test>
229
+ <dryrun>0</dryrun>
230
+ <log>0</log>
231
+ </test>
232
  </abandonedcartsconfig>
233
  </default>
234
 
app/code/community/DigitalPianism/Abandonedcarts/etc/system.xml CHANGED
@@ -16,9 +16,9 @@
16
  <show_in_store>1</show_in_store>
17
  <groups>
18
  <options translate="label">
19
- <label>Abandoned Carts Email</label>
20
  <frontend_type>text</frontend_type>
21
- <sort_order>110</sort_order>
22
  <show_in_default>1</show_in_default>
23
  <show_in_website>1</show_in_website>
24
  <show_in_store>1</show_in_store>
@@ -26,26 +26,66 @@
26
  <enable translate="label">
27
  <label>Enable Abandoned Carts Notification</label>
28
  <frontend_type>select</frontend_type>
29
- <source_model>adminhtml/system_config_source_yesno</source_model>
30
  <sort_order>10</sort_order>
31
  <show_in_default>1</show_in_default>
32
  <show_in_website>1</show_in_website>
33
  <show_in_store>1</show_in_store>
34
  </enable>
 
 
 
 
 
 
 
 
 
35
  <cron_expr translate="label">
36
  <label>Cron Schedule</label>
37
  <frontend_type>text</frontend_type>
38
- <sort_order>15</sort_order>
39
  <show_in_default>1</show_in_default>
40
  <show_in_website>1</show_in_website>
41
  <show_in_store>1</show_in_store>
42
  </cron_expr>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  <name translate="label">
44
  <label>Sender Name</label>
45
  <frontend_type>text</frontend_type>
46
  <backend_model>adminhtml/system_config_backend_email_sender</backend_model>
47
- <validate>validate-emailSender</validate>
48
- <sort_order>20</sort_order>
49
  <show_in_default>1</show_in_default>
50
  <show_in_website>1</show_in_website>
51
  <show_in_store>1</show_in_store>
@@ -55,7 +95,7 @@
55
  <frontend_type>text</frontend_type>
56
  <validate>validate-email</validate>
57
  <backend_model>adminhtml/system_config_backend_email_address</backend_model>
58
- <sort_order>30</sort_order>
59
  <show_in_default>1</show_in_default>
60
  <show_in_website>1</show_in_website>
61
  <show_in_store>1</show_in_store>
@@ -64,78 +104,101 @@
64
  <label>Email Template for Unaltered Abandoned Carts</label>
65
  <frontend_type>select</frontend_type>
66
  <source_model>adminhtml/system_config_source_email_template</source_model>
67
- <sort_order>40</sort_order>
68
  <show_in_default>1</show_in_default>
69
  <show_in_website>1</show_in_website>
70
  <show_in_store>1</show_in_store>
71
  </email_template>
72
- <notify_delay translate="label comment">
73
- <label>Send Abandoned Cart Email After</label>
74
- <frontend_type>text</frontend_type>
75
- <validate>validate-not-negative-number</validate>
76
- <sort_order>50</sort_order>
77
  <show_in_default>1</show_in_default>
78
  <show_in_website>1</show_in_website>
79
  <show_in_store>1</show_in_store>
80
- <comment>(days). NB: this only affects unaltered abandoned carts. If products go on sale, the email below is sent on the same day.</comment>
81
- </notify_delay>
82
- <enable_sale translate="label">
83
- <label>Enable Sale Abandoned Carts Notification</label>
84
  <frontend_type>select</frontend_type>
85
- <source_model>adminhtml/system_config_source_yesno</source_model>
86
- <sort_order>60</sort_order>
87
  <show_in_default>1</show_in_default>
88
  <show_in_website>1</show_in_website>
89
  <show_in_store>1</show_in_store>
90
- </enable_sale>
91
- <email_template_sale translate="label">
92
- <label>Email Template for Sale Products</label>
 
 
 
 
 
 
 
 
 
 
 
93
  <frontend_type>select</frontend_type>
94
- <source_model>adminhtml/system_config_source_email_template</source_model>
95
- <sort_order>70</sort_order>
96
  <show_in_default>1</show_in_default>
97
  <show_in_website>1</show_in_website>
98
  <show_in_store>1</show_in_store>
99
- </email_template_sale>
100
- <customer_groups translate="label">
101
- <label>Customer Groups Restriction</label>
102
- <frontend_type>multiselect</frontend_type>
103
- <source_model>adminhtml/system_config_source_customer_group_multiselect</source_model>
104
- <sort_order>75</sort_order>
105
  <show_in_default>1</show_in_default>
106
  <show_in_website>1</show_in_website>
107
  <show_in_store>1</show_in_store>
108
- </customer_groups>
 
 
 
 
 
 
 
 
 
 
 
 
109
  <dryrun translate="label comment">
110
  <label>Dry Run</label>
111
  <frontend_type>select</frontend_type>
112
- <source_model>adminhtml/system_config_source_yesno</source_model>
113
- <sort_order>80</sort_order>
114
  <show_in_default>1</show_in_default>
115
  <show_in_website>1</show_in_website>
116
  <show_in_store>1</show_in_store>
117
- <comment>Setting this parameter to Yes will log all the email addresses supposed to receive a notification into the var/log/digitalpianism_abandonedcarts.log file and will not send the real email notification</comment>
118
  </dryrun>
119
  <testemail translate="label comment">
120
  <label>Test Email</label>
121
  <frontend_type>text</frontend_type>
122
- <sort_order>90</sort_order>
123
  <show_in_default>1</show_in_default>
124
  <show_in_website>1</show_in_website>
125
  <show_in_store>1</show_in_store>
126
- <comment>With dry run set to yes, this email is used to filter the emails supposed to be sent and only send a notification email to the customer with this email address</comment>
 
127
  </testemail>
128
- <send translate="label">
129
- <label>Send Notifications Now</label>
130
- <frontend_type>button</frontend_type>
131
- <frontend_model>abandonedcarts/adminhtml_system_config_form_button</frontend_model>
132
- <sort_order>100</sort_order>
133
  <show_in_default>1</show_in_default>
134
  <show_in_website>1</show_in_website>
135
  <show_in_store>1</show_in_store>
136
- </send>
 
137
  </fields>
138
- </options>
139
  </groups>
140
  </abandonedcartsconfig>
141
  </sections>
16
  <show_in_store>1</show_in_store>
17
  <groups>
18
  <options translate="label">
19
+ <label>Global Configuration</label>
20
  <frontend_type>text</frontend_type>
21
+ <sort_order>1</sort_order>
22
  <show_in_default>1</show_in_default>
23
  <show_in_website>1</show_in_website>
24
  <show_in_store>1</show_in_store>
26
  <enable translate="label">
27
  <label>Enable Abandoned Carts Notification</label>
28
  <frontend_type>select</frontend_type>
29
+ <source_model>adminhtml/system_config_source_yesno</source_model>
30
  <sort_order>10</sort_order>
31
  <show_in_default>1</show_in_default>
32
  <show_in_website>1</show_in_website>
33
  <show_in_store>1</show_in_store>
34
  </enable>
35
+ <enable_sale translate="label">
36
+ <label>Enable Sale Abandoned Carts Notification</label>
37
+ <frontend_type>select</frontend_type>
38
+ <source_model>adminhtml/system_config_source_yesno</source_model>
39
+ <sort_order>20</sort_order>
40
+ <show_in_default>1</show_in_default>
41
+ <show_in_website>1</show_in_website>
42
+ <show_in_store>1</show_in_store>
43
+ </enable_sale>
44
  <cron_expr translate="label">
45
  <label>Cron Schedule</label>
46
  <frontend_type>text</frontend_type>
47
+ <sort_order>30</sort_order>
48
  <show_in_default>1</show_in_default>
49
  <show_in_website>1</show_in_website>
50
  <show_in_store>1</show_in_store>
51
  </cron_expr>
52
+ <notify_delay translate="label comment">
53
+ <label>Delay / Send Abandoned Cart Email After</label>
54
+ <frontend_type>text</frontend_type>
55
+ <validate>validate-not-negative-number</validate>
56
+ <sort_order>40</sort_order>
57
+ <show_in_default>1</show_in_default>
58
+ <show_in_website>1</show_in_website>
59
+ <show_in_store>1</show_in_store>
60
+ <depends><enable>1</enable></depends>
61
+ <comment>(days). NB: this only affects unaltered abandoned carts. If products go on sale, the email below is sent on the same day.</comment>
62
+ </notify_delay>
63
+ <customer_groups translate="label">
64
+ <label>Customer Groups Restriction</label>
65
+ <frontend_type>multiselect</frontend_type>
66
+ <source_model>adminhtml/system_config_source_customer_group_multiselect</source_model>
67
+ <sort_order>50</sort_order>
68
+ <show_in_default>1</show_in_default>
69
+ <show_in_website>1</show_in_website>
70
+ <show_in_store>1</show_in_store>
71
+ <depends><enable>1</enable></depends>
72
+ </customer_groups>
73
+ </fields>
74
+ </options>
75
+ <email translate="label">
76
+ <label>Email Configuration</label>
77
+ <frontend_type>text</frontend_type>
78
+ <sort_order>2</sort_order>
79
+ <show_in_default>1</show_in_default>
80
+ <show_in_website>1</show_in_website>
81
+ <show_in_store>1</show_in_store>
82
+ <fields>
83
  <name translate="label">
84
  <label>Sender Name</label>
85
  <frontend_type>text</frontend_type>
86
  <backend_model>adminhtml/system_config_backend_email_sender</backend_model>
87
+ <validate>validate-emailSender</validate>
88
+ <sort_order>10</sort_order>
89
  <show_in_default>1</show_in_default>
90
  <show_in_website>1</show_in_website>
91
  <show_in_store>1</show_in_store>
95
  <frontend_type>text</frontend_type>
96
  <validate>validate-email</validate>
97
  <backend_model>adminhtml/system_config_backend_email_address</backend_model>
98
+ <sort_order>20</sort_order>
99
  <show_in_default>1</show_in_default>
100
  <show_in_website>1</show_in_website>
101
  <show_in_store>1</show_in_store>
104
  <label>Email Template for Unaltered Abandoned Carts</label>
105
  <frontend_type>select</frontend_type>
106
  <source_model>adminhtml/system_config_source_email_template</source_model>
107
+ <sort_order>30</sort_order>
108
  <show_in_default>1</show_in_default>
109
  <show_in_website>1</show_in_website>
110
  <show_in_store>1</show_in_store>
111
  </email_template>
112
+ <email_template_sale translate="label">
113
+ <label>Email Template for Abandoned Carts Sale</label>
114
+ <frontend_type>select</frontend_type>
115
+ <source_model>adminhtml/system_config_source_email_template</source_model>
116
+ <sort_order>40</sort_order>
117
  <show_in_default>1</show_in_default>
118
  <show_in_website>1</show_in_website>
119
  <show_in_store>1</show_in_store>
120
+ </email_template_sale>
121
+ <autologin translate="label comment">
122
+ <label>Enable Automatic login link in the email</label>
 
123
  <frontend_type>select</frontend_type>
124
+ <source_model>adminhtml/system_config_source_yesno</source_model>
125
+ <sort_order>50</sort_order>
126
  <show_in_default>1</show_in_default>
127
  <show_in_website>1</show_in_website>
128
  <show_in_store>1</show_in_store>
129
+ <comment>Autologin links are one use only and expire after 48 hours for security reasons.</comment>
130
+ </autologin>
131
+ </fields>
132
+ </email>
133
+ <campaign translate="label">
134
+ <label>Google Campaign Tracker</label>
135
+ <frontend_type>text</frontend_type>
136
+ <sort_order>3</sort_order>
137
+ <show_in_default>1</show_in_default>
138
+ <show_in_website>1</show_in_website>
139
+ <show_in_store>1</show_in_store>
140
+ <fields>
141
+ <enable translate="label comment">
142
+ <label>Enable</label>
143
  <frontend_type>select</frontend_type>
144
+ <source_model>adminhtml/system_config_source_yesno</source_model>
145
+ <sort_order>10</sort_order>
146
  <show_in_default>1</show_in_default>
147
  <show_in_website>1</show_in_website>
148
  <show_in_store>1</show_in_store>
149
+ </enable>
150
+ <name translate="label comment">
151
+ <label>Campaign Name</label>
152
+ <frontend_type>text</frontend_type>
153
+ <sort_order>20</sort_order>
 
154
  <show_in_default>1</show_in_default>
155
  <show_in_website>1</show_in_website>
156
  <show_in_store>1</show_in_store>
157
+ <depends><enable>1</enable></depends>
158
+ <comment>utm_campaign</comment>
159
+ </name>
160
+ </fields>
161
+ </campaign>
162
+ <test translate="label">
163
+ <label>Test and debug</label>
164
+ <frontend_type>text</frontend_type>
165
+ <sort_order>3</sort_order>
166
+ <show_in_default>1</show_in_default>
167
+ <show_in_website>1</show_in_website>
168
+ <show_in_store>1</show_in_store>
169
+ <fields>
170
  <dryrun translate="label comment">
171
  <label>Dry Run</label>
172
  <frontend_type>select</frontend_type>
173
+ <source_model>adminhtml/system_config_source_yesno</source_model>
174
+ <sort_order>10</sort_order>
175
  <show_in_default>1</show_in_default>
176
  <show_in_website>1</show_in_website>
177
  <show_in_store>1</show_in_store>
178
+ <comment>Setting this parameter to Yes will not send the real email notification.</comment>
179
  </dryrun>
180
  <testemail translate="label comment">
181
  <label>Test Email</label>
182
  <frontend_type>text</frontend_type>
183
+ <sort_order>20</sort_order>
184
  <show_in_default>1</show_in_default>
185
  <show_in_website>1</show_in_website>
186
  <show_in_store>1</show_in_store>
187
+ <depends><dryrun>1</dryrun></depends>
188
+ <comment>With dry run set to yes, this email is used as a recipient email replacement so the real customer don't receive the email.</comment>
189
  </testemail>
190
+ <log translate="label comment">
191
+ <label>Enable Log</label>
192
+ <frontend_type>select</frontend_type>
193
+ <source_model>adminhtml/system_config_source_yesno</source_model>
194
+ <sort_order>30</sort_order>
195
  <show_in_default>1</show_in_default>
196
  <show_in_website>1</show_in_website>
197
  <show_in_store>1</show_in_store>
198
+ <comment>Setting this parameter to Yes will log abandoned cart actions so it can be viewed in Digital Pianism > Abandoned Carts > Logs</comment>
199
+ </log>
200
  </fields>
201
+ </test>
202
  </groups>
203
  </abandonedcartsconfig>
204
  </sections>
app/code/community/DigitalPianism/Abandonedcarts/sql/abandonedcarts_setup/upgrade-0.3.6-1.0.0.php ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $installer = $this;
4
+
5
+ $installer->startSetup();
6
+
7
+ $installer->run("
8
+ CREATE TABLE IF NOT EXISTS {$this->getTable('abandonedcarts/log')} (
9
+ `log_id` int(11) NOT NULL auto_increment,
10
+ `customer_email` varchar(255) default NULL,
11
+ `type` int(11) default NULL,
12
+ `comment` text default NULL,
13
+ `store` int(11) default NULL,
14
+ `dryrun` varchar(255) default NULL,
15
+ `added` timestamp NOT NULL default CURRENT_TIMESTAMP,
16
+ PRIMARY KEY (`log_id`)
17
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8;
18
+
19
+ CREATE TABLE IF NOT EXISTS {$this->getTable('abandonedcarts/link')} (
20
+ `link_id` int(11) NOT NULL auto_increment,
21
+ `token_hash` text default NULL,
22
+ `customer_email` varchar(255) default NULL,
23
+ `expiration_date` datetime default NULL,
24
+ PRIMARY KEY (`link_id`)
25
+ ) ENGINE = InnoDB DEFAULT CHARSET = utf8;");
26
+
27
+ $installer->endSetup();
app/design/adminhtml/default/default/layout/digitalpianism/abandonedcarts.xml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <layout version="0.1.0">
3
+ <adminhtml_abandonedcarts_grid>
4
+ <block type="abandonedcarts/adminhtml_abandonedcarts_grid" name="root" output="toHtml"/>
5
+ </adminhtml_abandonedcarts_grid>
6
+
7
+ <adminhtml_abandonedcarts_salegrid>
8
+ <block type="abandonedcarts/adminhtml_saleabandonedcarts_grid" name="root" output="toHtml"/>
9
+ </adminhtml_abandonedcarts_salegrid>
10
+ </layout>
app/design/adminhtml/default/default/template/digitalpianism/abandonedcarts/list.phtml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="content-header">
2
+ <table cellspacing="0">
3
+ <tr>
4
+ <td style="<?php echo $this->getHeaderWidth() ?>"><?php echo $this->getHeaderHtml() ?></td>
5
+ <td class="form-buttons"><?php echo $this->getButtonsHtml() ?></td>
6
+ </tr>
7
+ </table>
8
+ </div>
9
+ <?php echo $this->getStoreSwitcherHtml() ?>
10
+ <div>
11
+ <?php echo $this->getGridHtml() ?>
12
+ </div>
app/design/adminhtml/default/default/template/digitalpianism/abandonedcarts/system/config/button.phtml DELETED
@@ -1,17 +0,0 @@
1
- <script type="text/javascript">
2
- //<![CDATA[
3
- function send() {
4
- new Ajax.Request('<?php echo $this->getAjaxCheckUrl() ?>', {
5
- method: 'get',
6
- onSuccess: function(transport){
7
-
8
- if (transport.responseText){
9
- alert('Abandoned carts notifications have been sent')
10
- }
11
- }
12
- });
13
- }
14
- //]]>
15
- </script>
16
-
17
- <?php echo $this->getButtonHtml() ?>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/design/frontend/base/default/template/digitalpianism/abandonedcarts/email/items.phtml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ $productNames = $this->getProductname();
3
+ $productImages = $this->getProductimage();
4
+ ?>
5
+ <table>
6
+ <tr>
7
+ <th>
8
+ <?php echo Mage::helper('abandonedcarts')->__('Product Image'); ?>
9
+ </th>
10
+ <th>
11
+ <?php echo Mage::helper('abandonedcarts')->__('Product Name'); ?>
12
+ </th>
13
+ </tr>
14
+ <?php foreach($productNames as $i => $name): ?>
15
+ <tr>
16
+ <td>
17
+ <?php if ($productImages[$i]): ?>
18
+ <img src="<?php echo $productImages[$i]; ?>" alt="<?php echo $name; ?>" />
19
+ <?php endif; ?>
20
+ </td>
21
+ <td>
22
+ <?php echo $name; ?>
23
+ </td>
24
+ </tr>
25
+ <?php endforeach; ?>
26
+ </table>
app/design/frontend/base/default/template/digitalpianism/abandonedcarts/email/sale_items.phtml ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ $productNames = $this->getProductname();
3
+ $productImages = $this->getProductimage();
4
+ $cartPrices = $this->getCartprice();
5
+ $salePrices = $this->getSpecialprice();
6
+ ?>
7
+ <table>
8
+ <tr>
9
+ <th>
10
+ <?php echo Mage::helper('abandonedcarts')->__('Product Image'); ?>
11
+ </th>
12
+ <th>
13
+ <?php echo Mage::helper('abandonedcarts')->__('Product Name'); ?>
14
+ </th>
15
+ <th>
16
+ <?php echo Mage::helper('abandonedcarts')->__('Price when you left'); ?>
17
+ </th>
18
+ <th>
19
+ <?php echo Mage::helper('abandonedcarts')->__('Price now'); ?>
20
+ </th>
21
+ </tr>
22
+ <?php foreach($productNames as $i => $name): ?>
23
+ <tr>
24
+ <td>
25
+ <?php if ($productImages[$i]): ?>
26
+ <img src="<?php echo $productImages[$i]; ?>" alt="<?php echo $name; ?>" />
27
+ <?php endif; ?>
28
+ </td>
29
+ <td>
30
+ <?php echo $name; ?>
31
+ </td>
32
+ <td>
33
+ <?php echo $cartPrices[$i]; ?>
34
+ </td>
35
+ <td>
36
+ <?php echo $salePrices[$i]; ?>
37
+ </td>
38
+ </tr>
39
+ <?php endforeach; ?>
40
+ </table>
app/locale/en_US/DigitalPianism_Abandonedcarts.csv CHANGED
@@ -3,15 +3,43 @@ Abandoned Carts Emails,Abandoned Carts Emails
3
  Enable Abandoned Carts Notification,Enable Abandoned Carts Notification
4
  Enable Sale Abandoned Carts Notification,Enable Sale Abandoned Carts Notification
5
  Dry Run,Dry Run
6
- Setting this parameter to Yes will log all the email addresses supposed to receive a notification into the var/log/digitalpianism_abandonedcarts.log file and will not send the real email notification,Setting this parameter to Yes will log all the email addresses supposed to receive a notification into the var/log/digitalpianism_abandonedcarts.log file and will not send the real email notification
7
  Test Email,Test Email
8
- With dry run set to yes, this email is used to filter the emails supposed to be sent and only send a notification email to the customer with this email address,With dry run set to yes, this email is used to filter the emails supposed to be sent and only send a notification email to the customer with this email address
9
  Send Notifications Now,Send Notifications Now
10
  Cron Schedule,Cron Schedule
11
  Sender Name,Sender Name
12
  Sender Email,Sender Email
13
  Email Template for Unaltered Abandoned Carts,Email Template for Unaltered Abandoned Carts
14
- Send Abandoned Cart Email After,Send Abandoned Cart Email After
15
  (days). NB: this only affects unaltered abandoned carts. If products go on sale, the email below is sent on the same day.,(days). NB: this only affects unaltered abandoned carts. If products go on sale, the email below is sent on the same day.
16
  Email Template for Sale Products,Email Template for Sale Products
17
- Customer Groups Restriction,Customer Groups Restriction
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  Enable Abandoned Carts Notification,Enable Abandoned Carts Notification
4
  Enable Sale Abandoned Carts Notification,Enable Sale Abandoned Carts Notification
5
  Dry Run,Dry Run
 
6
  Test Email,Test Email
 
7
  Send Notifications Now,Send Notifications Now
8
  Cron Schedule,Cron Schedule
9
  Sender Name,Sender Name
10
  Sender Email,Sender Email
11
  Email Template for Unaltered Abandoned Carts,Email Template for Unaltered Abandoned Carts
 
12
  (days). NB: this only affects unaltered abandoned carts. If products go on sale, the email below is sent on the same day.,(days). NB: this only affects unaltered abandoned carts. If products go on sale, the email below is sent on the same day.
13
  Email Template for Sale Products,Email Template for Sale Products
14
+ Customer Groups Restriction,Customer Groups Restriction
15
+ With dry run set to yes, this email is used as a recipient email replacement so the real customer don't receive the email.,With dry run set to yes, this email is used as a recipient email replacement so the real customer don't receive the email.
16
+ Setting this parameter to Yes will not send the real email notification.,Setting this parameter to Yes will not send the real email notification.
17
+ Delay / Send Abandoned Cart Email After,Delay / Send Abandoned Cart Email After
18
+ Abandoned Carts (Applied delay: %s days),Abandoned Carts (Applied delay: %s days)
19
+ Send notifications,Send notifications
20
+ Abandoned Carts Logs,Abandoned Carts Logs
21
+ Sale Abandoned Carts,Sale Abandoned Carts
22
+ Customer Email,Customer Email
23
+ Customer Firstname,Customer Firstname
24
+ Customer Lastname,Customer Lastname
25
+ Product Ids,Product Ids
26
+ Product Names,Product Names
27
+ Cart Total,Cart Total
28
+ Cart Updated At,Cart Updated At
29
+ Send notification,Send notification
30
+ Type,Type
31
+ Comment,Comment
32
+ Store #,Store #
33
+ Cart Regular Total,Cart Regular Total
34
+ Cart Sale Total,Cart Sale Total
35
+ %sTotal of %d customer(s) were successfully notified,%sTotal of %d customer(s) were successfully notified
36
+ Logs,Logs
37
+ Abandoned Carts List,Abandoned Carts List
38
+ Abandoned Carts Sale List,Abandoned Carts Sale List
39
+ Enable Automatic login link in the email,Enable Automatic login link in the email
40
+ Autologin links are one use only and expire after 48 hours for security reasons.,Autologin links are one use only and expire after 48 hours for security reasons.
41
+ Google Campaign Tracker,Google Campaign Tracker
42
+ Campaign Name,Campaign Name
43
+ Test and debug,Test and debug
44
+ Enable Log,Enable Log
45
+ Setting this parameter to Yes will log abandoned cart actions so it can be viewed in Digital Pianism > Abandoned Carts > Logs,Setting this parameter to Yes will log abandoned cart actions so it can be viewed in Digital Pianism > Abandoned Carts > Logs
app/locale/en_US/template/email/digitalpianism/abandonedcarts/sales_abandonedcarts.html CHANGED
@@ -14,11 +14,9 @@
14
  <td valign="top">
15
  <div>
16
  <p>Dear {{var firstname}},</p>
17
- <p>You have abandoned the following product in your shopping bag:</p>
18
- <img src="{{var productimage}}" alt="{{var productname}}" />
19
- <p>{{var productname}}</p>
20
- <p>{{depend extraproductcount}} As well as {{var extraproductcount}} more products{{/depend}}.</p>
21
- <p>Follow this link and log in to finalize your purchase: {{config path="web/unsecure/base_url"}}</p>
22
  </div>
23
  </td>
24
  </tr>
14
  <td valign="top">
15
  <div>
16
  <p>Dear {{var firstname}},</p>
17
+ <p>You have abandoned the following product(s) in your shopping bag:</p>
18
+ {{block type='core/template' area='frontend' template='digitalpianism/abandonedcarts/email/items.phtml' productname=$productname productimage=$productimage}}
19
+ <p>Follow this link and log in to finalize your purchase: <a href="{{var link}}">{{var link}}</a></p>
 
 
20
  </div>
21
  </td>
22
  </tr>
app/locale/en_US/template/email/digitalpianism/abandonedcarts/sales_abandonedcarts_sale.html CHANGED
@@ -14,16 +14,10 @@
14
  <td valign="top">
15
  <div>
16
  <p>Dear {{var firstname}},</p>
17
- <p>You have abandoned the following product in your shopping bag:</p>
18
- <img src="{{var productimage}}" alt="{{var productname}}" />
19
- <p>{{var productname}}</p>
20
- <p>It was {{var cartprice}} and now is {{var specialprice}}.</p>
21
- <p>
22
- {{depend extraproductcount}}
23
- Purchase the {{var extraproductcount}} other sale products in your cart and save {{var discount}} on your order.
24
- {{/depend}}
25
- </p>
26
- <p>Follow this link and log in to finalize your purchase with the new special price: {{config path="web/unsecure/base_url"}}</p>
27
  </div>
28
  </td>
29
  </tr>
14
  <td valign="top">
15
  <div>
16
  <p>Dear {{var firstname}},</p>
17
+ <p>You have abandoned the following product(s) in your shopping bag and they are now on sale:</p>
18
+ {{block type='core/template' area='frontend' template='digitalpianism/abandonedcarts/email/sale_items.phtml' productname=$productname productimage=$productimage cartprice=$cartprice specialprice=$specialprice}}
19
+ <p>Purchase every product in your cart and save {{var discount}} on your order.</p>
20
+ <p>Follow this link to finalize your purchase with the new special price: <a href="{{var link}}">{{var link}}</a></p>
 
 
 
 
 
 
21
  </div>
22
  </td>
23
  </tr>
app/locale/fr_FR/DigitalPianism_Abandonedcarts.csv CHANGED
@@ -3,15 +3,43 @@ Abandoned Carts Emails,Emails des paniers abandonn&eacute;s
3
  Enable Abandoned Carts Notification,Activer les notifications de paniers abandonn&eacute;s
4
  Enable Sale Abandoned Carts Notification,Activer les notifications de paniers abandonn&eacute;s en promo
5
  Dry Run,Test
6
- Setting this parameter to Yes will log all the email addresses supposed to receive a notification into the var/log/digitalpianism_abandonedcarts.log file and will not send the real email notification,Mettre la valeur &agrave; Oui enregistrera toutes les adresses emails suppos&eacute;es recevoir une notification dans le fichier var/log/digitalpianism_abandonedcarts.log et n'enverra pas les emails
7
  Test Email,Email de test
8
- With dry run set to yes, this email is used to filter the emails supposed to be sent and only send a notification email to the customer with this email address,Avec Test &agrave; Oui, cette adresse email sera utilis&eacute;e pour filter les emails suppos&eacute;s &ecirc;tre envoy&eacute; et seulement envoy&eacute; l'email de notification au client avec cette adresse
9
  Send Notifications Now,Envoyer les notification maintenant
10
  Cron Schedule,R&eacute;glage du cron
11
  Sender Name,Nom de l'exp&eacute;diteur
12
  Sender Email,Email de l'exp&eacute;diteur
13
  Email Template for Unaltered Abandoned Carts,Gabarit d'email pour les paniers abandonn&eacute;s non modifi&eacute;s
14
- Send Abandoned Cart Email After,Envoyer l'email de notification apr&egrave;s
15
- (days). NB: this only affects unaltered abandoned carts. If products go on sale, the email below is sent on the same day.,(jours). NB: cela affecte seulement les paniers abandonn&eacute;s non modifi&eacute;s. Si un des produits passent en promotion, l'email ci dessous sera envoy&eacute; le m�me jour.
16
  Email Template for Sale Products,Gabarit d'email pour les produits en promo
17
- Customer Groups Restriction,Restriction de groupes clients
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  Enable Abandoned Carts Notification,Activer les notifications de paniers abandonn&eacute;s
4
  Enable Sale Abandoned Carts Notification,Activer les notifications de paniers abandonn&eacute;s en promo
5
  Dry Run,Test
 
6
  Test Email,Email de test
 
7
  Send Notifications Now,Envoyer les notification maintenant
8
  Cron Schedule,R&eacute;glage du cron
9
  Sender Name,Nom de l'exp&eacute;diteur
10
  Sender Email,Email de l'exp&eacute;diteur
11
  Email Template for Unaltered Abandoned Carts,Gabarit d'email pour les paniers abandonn&eacute;s non modifi&eacute;s
12
+ (days). NB: this only affects unaltered abandoned carts. If products go on sale, the email below is sent on the same day.,(jours). NB: cela affecte seulement les paniers abandonn&eacute;s non modifi&eacute;s. Si un des produits passent en promotion, l'email ci dessous sera envoy&eacute; le m�me jour.
 
13
  Email Template for Sale Products,Gabarit d'email pour les produits en promo
14
+ Customer Groups Restriction,Restriction de groupes clients
15
+ With dry run set to yes, this email is used as a recipient email replacement so the real customer don't receive the email.,Avec Test à Oui, cet email sera utilis&eacute; en remplacement de l'email du client afin qu'il ne recoive pas l'email
16
+ Setting this parameter to Yes will not send the real email notification.,Si oui, les emails ne seront pas envoyés
17
+ Delay / Send Abandoned Cart Email After,Delai / Envoyer les emails de notifications apr&eacute;s
18
+ Abandoned Carts (Applied delay: %s days),Paniers Abandonn&eacute;s (Delai appliqu&eacute;: %s jours)
19
+ Send notifications,Envoyer les notifications
20
+ Abandoned Carts Logs,Logs de paniers abandonn&eacute;s
21
+ Sale Abandoned Carts,Paniers abandonn&eacute;s en promotion
22
+ Customer Email,Email client
23
+ Customer Firstname,Pr&eacute;nom client
24
+ Customer Lastname,Nom de famille client
25
+ Product Ids,Ids Produits
26
+ Product Names,Noms Produits
27
+ Cart Total,Total Panier
28
+ Cart Updated At,Mise &agrave; jour panier
29
+ Send notification,Envoyer notification
30
+ Type,Type
31
+ Comment,Commentaire
32
+ Store #,Boutique #
33
+ Cart Regular Total,Total panier
34
+ Cart Sale Total,Total panier en promotion
35
+ %sTotal of %d customer(s) were successfully notified,%s%d client(s) ont bien &eacute;t&eacute; notifi&eacute;
36
+ Logs,Logs
37
+ Abandoned Carts List,Liste des paniers abandonn&eacute;s
38
+ Abandoned Carts Sale List,Liste des paniers abandonn&eacute;s en promotion
39
+ Enable Automatic login link in the email,Activer le lien de connexion automatique dans l'email
40
+ Autologin links are one use only and expire after 48 hours for security reasons.,Les liens de connexion automatiques sont &agrave; usage unique et expirent au bout de 48 heures pour des raisons de s&eacute;rit&eacute;
41
+ Google Campaign Tracker,Tracker Google Campaign
42
+ Campaign Name,Nom de la campagne
43
+ Test and debug,Test et d&eacute;boguage
44
+ Enable Log,Activer les logs
45
+ Setting this parameter to Yes will log abandoned cart actions so it can be viewed in Digital Pianism > Abandoned Carts > Logs,Si oui, les logs seront visualisables dans Digital Pianism > Paniers Abandonn&eacute;s > Logs
app/locale/fr_FR/template/email/digitalpianism/abandonedcarts/sales_abandonedcarts.html CHANGED
@@ -12,12 +12,10 @@
12
  <!-- [ middle starts here] -->
13
  <tr>
14
  <td valign="top">
15
- <p style="font-size:12px; line-height:16px; margin:0;">
16
- Ch&egrave;r(e) {{var firstname}},<br />
17
- Vous avez abandonn&eacute; un(e) {{var productname}} {{depend extraproductcount}} et {{var extraproductcount}} autres produits{{/depend}} dans votre panier.<br/>
18
-
19
- Suivez ce lien et connectez-vous pour finaliser votre achat: {{config path="web/unsecure/base_url"}}
20
- </p>
21
  </td>
22
  </tr>
23
  </table>
12
  <!-- [ middle starts here] -->
13
  <tr>
14
  <td valign="top">
15
+ <p>Ch&egrave;r(e) {{var firstname}},</p>
16
+ <p>Vous avez abandonn&eacute; le(s) produit(s) suivant(s) dans votre panier.</p>
17
+ {{block type='core/template' area='frontend' template='digitalpianism/abandonedcarts/email/items.phtml' productname=$productname productimage=$productimage}}
18
+ <p>Suivez ce lien et connectez-vous pour finaliser votre achat: <a href="{{var link}}">{{var link}}</a></p>
 
 
19
  </td>
20
  </tr>
21
  </table>
app/locale/fr_FR/template/email/digitalpianism/abandonedcarts/sales_abandonedcarts_sale.html CHANGED
@@ -12,18 +12,11 @@
12
  <!-- [ middle starts here] -->
13
  <tr>
14
  <td valign="top">
15
- <p style="font-size:12px; line-height:16px; margin:0;">
16
- Ch&egrave;r(e) {{var firstname}},<br />
17
- Vous avez abandonn&eacute; un(e) {{var productname}} dans votre panier.<br/>
18
-
19
- Son prix &eacute;tait de {{var cartprice}} et est maintenant de {{var specialprice}}.<br/>
20
-
21
- {{depend extraproductcount}}
22
- Achetez les {{var extraproductcount}} autre produits en promotion dans votre panier et &eacute;conomisez {{var discount}} sur votre commande.<br/>
23
- {{/depend}}
24
-
25
- Suivez ce lien et connectez vous pour finaliser votre achat en profitant des promotions: {{config path="web/unsecure/base_url"}}
26
- </p>
27
  </td>
28
  </tr>
29
  </table>
12
  <!-- [ middle starts here] -->
13
  <tr>
14
  <td valign="top">
15
+ <p>Ch&egrave;r(e) {{var firstname}},</p>
16
+ <p>Vous avez abandonn&eacute; le(s) produit(s) suivant(s) dans votre panier et ils sont d&eacute;sormais en promotion.</p>
17
+ {{block type='core/template' area='frontend' template='digitalpianism/abandonedcarts/email/sale_items.phtml' productname=$productname productimage=$productimage cartprice=$cartprice specialprice=$specialprice}}
18
+ <p>Acheter les produits dans votre panier et &eacute;conomisez {{var discount}} sur votre commande.</p>
19
+ <p>Suivez ce lien et connectez vous pour finaliser votre achat en profitant des promotions: <a href="{{var link}}">{{var link}}</a></p>
 
 
 
 
 
 
 
20
  </td>
21
  </tr>
22
  </table>
package.xml CHANGED
@@ -1,7 +1,7 @@
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>DigitalPianism_Abandonedcarts</name>
4
- <version>0.3.6</version>
5
  <stability>stable</stability>
6
  <license uri="http://opensource.org/licenses/osl-3.0.php">OSL v3.0</license>
7
  <channel>community</channel>
@@ -95,11 +95,18 @@ Save the configuration.&#xD;
95
  &#xD;
96
  &lt;p&gt;To manually trigger the notification system, please access System &amp;gt; Configuration &amp;gt; Digital Pianism &amp;gt; Abandoned carts email and click on the "Send" button&lt;/p&gt;&#xD;
97
  &lt;p&gt;Please note that this functionality will send abandoned carts notification regardless the delay you provided, all possible abandoned carts emails will be sent.&lt;/p&gt;</description>
98
- <notes>- Fix a bug where an error would be logged in case the product image does not exist.</notes>
 
 
 
 
 
 
 
99
  <authors><author><name>Digital Pianism</name><user>digitalpianism</user><email>contact@digital-pianism.com</email></author></authors>
100
- <date>2016-04-22</date>
101
- <time>13:16:02</time>
102
- <contents><target name="magecommunity"><dir name="DigitalPianism"><dir name="Abandonedcarts"><dir name="Block"><dir name="Adminhtml"><dir name="System"><dir name="Config"><dir name="Form"><file name="Button.php" hash="1c8d9cad5c54bcc28c0760e72406b5e3"/></dir></dir></dir></dir></dir><dir name="Helper"><file name="Data.php" hash="5148fe5929e61f4c8385c6c5be0b89f3"/></dir><dir name="Model"><file name="Observer.php" hash="8f44dfb99958d521b10a292a65d842d4"/><dir name="Sales"><dir name="Resource"><file name="Quote.php" hash="3b2f9f24a74a6ea3b6851d64bd6ae5ba"/></dir></dir></dir><dir name="controllers"><dir name="Adminhtml"><file name="AbandonedcartsController.php" hash="c26ae0925cc1ca148f5e3277702842e2"/></dir></dir><dir name="etc"><file name="adminhtml.xml" hash="8ddca513c0ed7e034c476f3e026ceda8"/><file name="config.xml" hash="8d6a68ea652f6c578e91adffa33d40be"/><file name="system.xml" hash="07f261c1f35321317da8f09f75a37317"/></dir><dir name="sql"><dir name="abandonedcarts_setup"><file name="install-0.0.1.php" hash="851338e4a710b5d94fead688b065f4b5"/><file name="upgrade-0.0.1-0.0.2.php" hash="0227c009e49b97bcf3f34f84c49f0927"/></dir></dir></dir></dir></target><target name="mageetc"><dir name="modules"><file name="DigitalPianism_Abandonedcarts.xml" hash="8a7657855486c68d548db4ba48e083d2"/></dir></target><target name="magedesign"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="template"><dir name="digitalpianism"><dir name="abandonedcarts"><dir name="system"><dir name="config"><file name="button.phtml" hash="8f7e673ea52cd81b616cac01b1022990"/></dir></dir></dir></dir></dir></dir></dir></dir></target><target name="magelocale"><dir name="en_US"><dir name="template"><dir name="email"><dir name="digitalpianism"><dir name="abandonedcarts"><file name="sales_abandonedcarts.html" hash="f8a5ec3af09730f06ade1fd18fa321e9"/><file name="sales_abandonedcarts_sale.html" hash="22bb7a1e95e336948a43f282e7e58806"/></dir></dir></dir></dir><file name="DigitalPianism_Abandonedcarts.csv" hash="b782bff95dba8b860cd773a674aac6c9"/></dir><dir name="fr_FR"><dir name="template"><dir name="email"><dir name="digitalpianism"><dir name="abandonedcarts"><file name="sales_abandonedcarts.html" hash="5340ea06fbf9d2213ea2f09e7425181b"/><file name="sales_abandonedcarts_sale.html" hash="22592c5467a554ab80195218bec5b6b0"/></dir></dir></dir></dir><file name="DigitalPianism_Abandonedcarts.csv" hash="0f5271e2ad1d6b07061314b18bd170c2"/></dir></target></contents>
103
  <compatible/>
104
  <dependencies><required><php><min>4.1.0</min><max>6.0.0</max></php></required></dependencies>
105
  </package>
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>DigitalPianism_Abandonedcarts</name>
4
+ <version>1.0.0</version>
5
  <stability>stable</stability>
6
  <license uri="http://opensource.org/licenses/osl-3.0.php">OSL v3.0</license>
7
  <channel>community</channel>
95
  &#xD;
96
  &lt;p&gt;To manually trigger the notification system, please access System &amp;gt; Configuration &amp;gt; Digital Pianism &amp;gt; Abandoned carts email and click on the "Send" button&lt;/p&gt;&#xD;
97
  &lt;p&gt;Please note that this functionality will send abandoned carts notification regardless the delay you provided, all possible abandoned carts emails will be sent.&lt;/p&gt;</description>
98
+ <notes>- Full refactor of the module&#xD;
99
+ - Add two grids to the backend to see the abandoned carts&#xD;
100
+ - Add a log database table to easily see what's going on&#xD;
101
+ - Implement an autologin link system&#xD;
102
+ - Implement Google Campaign tags&#xD;
103
+ - Improve the templates to list all items&#xD;
104
+ - Change the way dryrun and test email behaves&#xD;
105
+ - Add notification flags columns to the native abandoned carts report</notes>
106
  <authors><author><name>Digital Pianism</name><user>digitalpianism</user><email>contact@digital-pianism.com</email></author></authors>
107
+ <date>2016-07-14</date>
108
+ <time>15:18:45</time>
109
+ <contents><target name="magecommunity"><dir name="DigitalPianism"><dir name="Abandonedcarts"><dir name="Block"><dir name="Adminhtml"><dir name="Abandonedcarts"><file name="Grid.php" hash="f0d080d29b7d784004ef412ed5a337d6"/></dir><file name="Abandonedcarts.php" hash="3c8d3c5676cb3bda46a580566f35fd9d"/><dir name="Logs"><file name="Grid.php" hash="05c4ca332a6ad168e28e9a9128252231"/></dir><file name="Logs.php" hash="1173ec175c365fa5c01cbc72a98c0284"/><dir name="Saleabandonedcarts"><file name="Grid.php" hash="cc43378214837f8f9c7c7a66dc47adf4"/></dir><file name="Saleabandonedcarts.php" hash="da29b0f8262da3ec0b993816c5fc5be0"/></dir></dir><dir name="Helper"><file name="Data.php" hash="f7d07930e3276bb06e6209293f287dc3"/></dir><dir name="Model"><dir name="Adminhtml"><file name="Observer.php" hash="d54b2bc70a87fca4f0c3bbb519b84c81"/></dir><file name="Collection.php" hash="3e19497dd2cd2170293136f8cff891dc"/><dir name="Link"><file name="Cleaner.php" hash="aced9e659252056b0f4747a78c6154c8"/></dir><file name="Link.php" hash="6f19c7976980e558d98589021d4d294f"/><file name="Log.php" hash="c9ce940c6a14cfa85c401183559661e4"/><file name="Notifier.php" hash="38c1fcb51994dc155ce62bbb38564526"/><dir name="Resource"><dir name="Link"><file name="Collection.php" hash="39ea2cfb265412d82b9fda822af6d324"/></dir><file name="Link.php" hash="49d00b249de30aefc978f4515f6dbdd7"/><dir name="Log"><file name="Collection.php" hash="54abd79af31a1a853bc08eeed75dc7d0"/></dir><file name="Log.php" hash="00edba4d934093236ac78b42f066ba73"/></dir><dir name="Sales"><dir name="Resource"><file name="Quote.php" hash="3b2f9f24a74a6ea3b6851d64bd6ae5ba"/></dir></dir></dir><dir name="controllers"><dir name="Adminhtml"><file name="AbandonedcartsController.php" hash="32a56a5033b64320879da37f8b2a88e8"/></dir><file name="IndexController.php" hash="5c06db338a20d3de9b19c3f606edbc9a"/></dir><dir name="data"><dir name="abandonedcarts_setup"><file name="data-upgrade-1.0.0-1.0.1.php" hash="a60f9bccf9e42a458f808bc697320bb0"/></dir></dir><dir name="etc"><file name="adminhtml.xml" hash="ce393eb00049f28ff92401be828cd613"/><file name="config.xml" hash="5d3d15cec7d41a670cba49116b01a109"/><file name="system.xml" hash="e6a53269f6223eb246c2495600eb307d"/></dir><dir name="sql"><dir name="abandonedcarts_setup"><file name="install-0.0.1.php" hash="851338e4a710b5d94fead688b065f4b5"/><file name="upgrade-0.0.1-0.0.2.php" hash="0227c009e49b97bcf3f34f84c49f0927"/><file name="upgrade-0.3.6-1.0.0.php" hash="1ac772ef331c8a2278e2c8df77aeb799"/></dir></dir></dir></dir></target><target name="mageetc"><dir name="modules"><file name="DigitalPianism_Abandonedcarts.xml" hash="8a7657855486c68d548db4ba48e083d2"/></dir></target><target name="magedesign"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="template"><dir name="digitalpianism"><dir name="abandonedcarts"><file name="list.phtml" hash="6af16de73f1b0a3c580e65a95642722f"/></dir></dir></dir><dir name="layout"><dir name="digitalpianism"><file name="abandonedcarts.xml" hash="2f4ec5178aed1c84213605b5212d676e"/></dir></dir></dir></dir></dir><dir name="frontend"><dir name="base"><dir name="default"><dir name="template"><dir name="digitalpianism"><dir name="abandonedcarts"><dir name="email"><file name="items.phtml" hash="1938d5ee30752918b1be76be845b9214"/><file name="sale_items.phtml" hash="7dfce25f17ba19532e68592772bf63ad"/></dir></dir></dir></dir></dir></dir></dir></target><target name="magelocale"><dir name="en_US"><dir name="template"><dir name="email"><dir name="digitalpianism"><dir name="abandonedcarts"><file name="sales_abandonedcarts.html" hash="a35bd61e1f172b37ac4ed317e1ad44e9"/><file name="sales_abandonedcarts_sale.html" hash="4f437deca852efeacfec0fb3ba929971"/></dir></dir></dir></dir><file name="DigitalPianism_Abandonedcarts.csv" hash="bd3ed00291684eac5149305ed829a824"/></dir><dir name="fr_FR"><dir name="template"><dir name="email"><dir name="digitalpianism"><dir name="abandonedcarts"><file name="sales_abandonedcarts.html" hash="3ec93757d563ed926090a394577f1dbd"/><file name="sales_abandonedcarts_sale.html" hash="3586968516c8e8374cfa913a3eea7995"/></dir></dir></dir></dir><file name="DigitalPianism_Abandonedcarts.csv" hash="2a9c63b4d83cb922b3060a4735dabe38"/></dir></target></contents>
110
  <compatible/>
111
  <dependencies><required><php><min>4.1.0</min><max>6.0.0</max></php></required></dependencies>
112
  </package>