DigitalPianism_Abandonedcarts - Version 0.1.3

Version Notes

- Add test email variable

Download this release

Release Info

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


Version 0.1.3

app/code/community/DigitalPianism/Abandonedcarts/Block/Adminhtml/System/Config/Form/Button.php ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class DigitalPianism_Abandonedcarts_Block_Adminhtml_System_Config_Form_Button extends Mage_Adminhtml_Block_System_Config_Form_Field
4
+ {
5
+ /*
6
+ * Set template
7
+ */
8
+ protected function _construct()
9
+ {
10
+ parent::_construct();
11
+ $this->setTemplate('digitalpianism/abandonedcarts/system/config/button.phtml');
12
+ }
13
+
14
+ /**
15
+ * Return element html
16
+ *
17
+ * @param Varien_Data_Form_Element_Abstract $element
18
+ * @return string
19
+ */
20
+ protected function _getElementHtml(Varien_Data_Form_Element_Abstract $element)
21
+ {
22
+ return $this->_toHtml();
23
+ }
24
+
25
+ /**
26
+ * Return ajax url for button
27
+ *
28
+ * @return string
29
+ */
30
+ public function getAjaxCheckUrl()
31
+ {
32
+ return Mage::helper('adminhtml')->getUrl('abandonedcarts_admin/adminhtml_abandonedcarts/send');
33
+ }
34
+
35
+ /**
36
+ * Generate button html
37
+ *
38
+ * @return string
39
+ */
40
+ public function getButtonHtml()
41
+ {
42
+ $button = $this->getLayout()->createBlock('adminhtml/widget_button')
43
+ ->setData(array(
44
+ 'id' => 'abandonedcarts_button',
45
+ 'label' => $this->helper('adminhtml')->__('Send'),
46
+ 'onclick' => 'javascript:send(); return false;'
47
+ ));
48
+
49
+ return $button->toHtml();
50
+ }
51
+ }
app/code/community/DigitalPianism/Abandonedcarts/Helper/Data.php ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class DigitalPianism_Abandonedcarts_Helper_Data extends Mage_Core_Helper_Abstract
4
+ {
5
+ protected $logFileName = 'digitalpianism_abandonedcarts.log';
6
+
7
+ /**
8
+ * Log data
9
+ * @param string|object|array data to log
10
+ */
11
+ public function log($data)
12
+ {
13
+ Mage::log($data, null, $this->logFileName);
14
+ }
15
+
16
+ public function isEnabled()
17
+ {
18
+ return Mage::getStoreConfig('abandonedcartsconfig/options/enable');
19
+ }
20
+
21
+ public function isSaleEnabled()
22
+ {
23
+ return Mage::getStoreConfig('abandonedcartsconfig/options/enable_sale');
24
+ }
25
+
26
+ public function getDryRun()
27
+ {
28
+ return Mage::getStoreConfig('abandonedcartsconfig/options/dryrun');
29
+ }
30
+
31
+ public function getTestEmail()
32
+ {
33
+ return Mage::getStoreConfig('abandonedcartsconfig/options/testemail');
34
+ }
35
+
36
+ }
app/code/community/DigitalPianism/Abandonedcarts/Model/Observer.php ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class DigitalPianism_Abandonedcarts_Model_Observer extends Mage_Core_Model_Abstract
4
+ {
5
+
6
+ /**
7
+ * Send notification email to customer with abandoned cart containing sale products
8
+ * @param boolean if dryrun is set to true, it won't send emails and won't alter quotes
9
+ * @param string email to test
10
+ */
11
+ public function sendAbandonedCartsSaleEmail($dryrun = false, $testemail = null)
12
+ {
13
+ if (Mage::helper('abandonedcarts')->getDryRun()) $dryrun = true;
14
+ if (Mage::helper('abandonedcarts')->getTestEmail()) $testemail = Mage::helper('abandonedcarts')->getTestEmail();
15
+
16
+ try
17
+ {
18
+ if (Mage::helper('abandonedcarts')->isSaleEnabled())
19
+ {
20
+ // Date handling
21
+ $store = Mage_Core_Model_App::ADMIN_STORE_ID;
22
+ $timezone = Mage::app()->getStore($store)->getConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE);
23
+ date_default_timezone_set($timezone);
24
+
25
+ // Current date
26
+ $currentdate = date("Ymd");
27
+
28
+ $day = (int)substr($currentdate,-2);
29
+ $month = (int)substr($currentdate,4,2);
30
+ $year = (int)substr($currentdate,0,4);
31
+
32
+ $date = array('year' => $year,'month' => $month,'day' => $day,'hour' => 23,'minute' => 59,'second' => 59);
33
+
34
+ $today = new Zend_Date($date);
35
+ $today->setTimeZone("UTC");
36
+
37
+ date_default_timezone_set($timezone);
38
+
39
+ $todayString = $today->toString("Y-MM-dd HH:mm:ss");
40
+
41
+ // Get the attribute id for the status attribute
42
+ $eavAttribute = new Mage_Eav_Model_Mysql4_Entity_Attribute();
43
+ $statusId = $eavAttribute->getIdByCode('catalog_product', 'status');
44
+
45
+ // Loop through the stores
46
+ foreach (Mage::app()->getWebsites() as $website) {
47
+ // Get the website id
48
+ $websiteId = $website->getWebsiteId();
49
+ foreach ($website->getGroups() as $group) {
50
+ $stores = $group->getStores();
51
+ foreach ($stores as $store) {
52
+
53
+ // Get the store id
54
+ $storeId = $store->getStoreId();
55
+
56
+ // Init the store to be able to load the quote and the collections properly
57
+ Mage::app()->init($storeId,'store');
58
+
59
+ // Get the product collection
60
+ $collection = Mage::getResourceModel('catalog/product_collection')->setStore($storeId);
61
+
62
+ // First collection: carts with products that became on sale
63
+ // Join the collection with the required tables
64
+ $collection->getSelect()
65
+ ->reset(Zend_Db_Select::COLUMNS)
66
+ ->columns(array('e.entity_id AS product_id',
67
+ 'e.sku',
68
+ 'catalog_flat.name as product_name',
69
+ 'catalog_flat.price as product_price',
70
+ 'catalog_flat.special_price as product_special_price',
71
+ 'catalog_flat.special_from_date as product_special_from_date',
72
+ 'catalog_flat.special_to_date as product_special_to_date',
73
+ 'quote_table.entity_id as cart_id',
74
+ 'quote_table.updated_at as cart_updated_at',
75
+ 'quote_table.abandoned_sale_notified as has_been_notified',
76
+ 'quote_items.price as product_price_in_cart',
77
+ 'quote_table.customer_email as customer_email',
78
+ 'quote_table.customer_firstname as customer_firstname',
79
+ 'quote_table.customer_lastname as customer_lastname'
80
+ )
81
+ )
82
+ ->joinInner(
83
+ array('quote_items' => 'sales_flat_quote_item'),
84
+ 'quote_items.product_id = e.entity_id AND quote_items.price > 0.00',
85
+ null)
86
+ ->joinInner(
87
+ array('quote_table' => 'sales_flat_quote'),
88
+ '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,
89
+ null)
90
+ ->joinInner(
91
+ array('catalog_flat' => 'catalog_product_flat_'.$storeId),
92
+ 'catalog_flat.entity_id = e.entity_id',
93
+ null)
94
+ ->joinInner(
95
+ array('catalog_enabled' => 'catalog_product_entity_int'),
96
+ 'catalog_enabled.entity_id = e.entity_id AND catalog_enabled.attribute_id = '.$statusId.' AND catalog_enabled.value = 1',
97
+ null)
98
+ ->joinInner(
99
+ array('inventory' => 'cataloginventory_stock_status'),
100
+ 'inventory.product_id = e.entity_id AND inventory.stock_status = 1 AND inventory.website_id = '.$websiteId,
101
+ null)
102
+ ->order('quote_table.updated_at DESC');
103
+
104
+ //echo $collection->printlogquery(true);
105
+ $collection->load();
106
+
107
+ // Skip the rest of the code if the collection is empty
108
+ if (count($collection) == 0)
109
+ {
110
+ continue;
111
+ }
112
+
113
+ // Recipients array
114
+ $recipients = array();
115
+
116
+ foreach($collection as $entry)
117
+ {
118
+ // Double check if the special from date is set
119
+ if (!$entry->getProductSpecialFromDate())
120
+ {
121
+ // If not we use today for the comparison
122
+ $fromDate = $todayString;
123
+ }
124
+ else $fromDate = $entry->getProductSpecialFromDate();
125
+
126
+ // Do the same for the special to date
127
+ if (!$entry->getProductSpecialToDate())
128
+ {
129
+ $toDate = $todayString;
130
+ }
131
+ else $toDate = $entry->getProductSpecialToDate();
132
+
133
+ // We need to ensure that the price in cart is higher than the new special price
134
+ // As well as the date comparison in case the sale is over or hasn't started
135
+ if ($entry->getProductPriceInCart() > 0.00
136
+ && $entry->getProductSpecialPrice() > 0.00
137
+ && ($entry->getProductPriceInCart() > $entry->getProductSpecialPrice())
138
+ && ($fromDate <= $todayString)
139
+ && ($toDate >= $todayString))
140
+ {
141
+
142
+ // Test if the customer is already in the array
143
+ if (!array_key_exists($entry->getCustomerEmail(), $recipients))
144
+ {
145
+ // Create an array of variables to assign to template
146
+ $emailTemplateVariables = array();
147
+
148
+ // Array that contains the data which will be used inside the template
149
+ $emailTemplateVariables['fullname'] = $entry->getCustomerFirstname().' '.$entry->getCustomerLastname();
150
+ $emailTemplateVariables['firstname'] = $entry->getCustomerFirstname();
151
+ $emailTemplateVariables['productname'] = $entry->getProductName();
152
+ $emailTemplateVariables['cartprice'] = number_format($entry->getProductPriceInCart(),2);
153
+ $emailTemplateVariables['specialprice'] = number_format($entry->getProductSpecialPrice(),2);
154
+
155
+ // Assign the values to the array of recipients
156
+ $recipients[$entry->getCustomerEmail()]['cartId'] = $entry->getCartId();
157
+ }
158
+ else
159
+ {
160
+ // We create some extra variables if there is several products in the cart
161
+ $emailTemplateVariables = $recipients[$entry->getCustomerEmail()]['emailTemplateVariables'];
162
+ // Discount amount
163
+ // If one product before
164
+ if (!array_key_exists('discount',$emailTemplateVariables))
165
+ {
166
+ $emailTemplateVariables['discount'] = $emailTemplateVariables['cartprice'] - $emailTemplateVariables['specialprice'];
167
+ }
168
+ // We add the discount on the second product
169
+ $moreDiscount = number_format($entry->getProductPriceInCart(),2) - number_format($entry->getProductSpecialPrice(),2);
170
+ $emailTemplateVariables['discount'] += $moreDiscount;
171
+ // We increase the product count
172
+ if (!array_key_exists('extraproductcount',$emailTemplateVariables))
173
+ {
174
+ $emailTemplateVariables['extraproductcount'] = 0;
175
+ }
176
+ $emailTemplateVariables['extraproductcount'] += 1;
177
+ }
178
+
179
+ // Add currency codes to prices
180
+ $emailTemplateVariables['cartprice'] = Mage::helper('core')->currency($emailTemplateVariables['cartprice'], true, false);
181
+ $emailTemplateVariables['specialprice'] = Mage::helper('core')->currency($emailTemplateVariables['specialprice'], true, false);
182
+ if (array_key_exists('discount',$emailTemplateVariables))
183
+ {
184
+ $emailTemplateVariables['discount'] = Mage::helper('core')->currency($emailTemplateVariables['discount'], true, false);
185
+ }
186
+
187
+ // Assign the array of template variables
188
+ $recipients[$entry->getCustomerEmail()]['emailTemplateVariables'] = $emailTemplateVariables;
189
+ }
190
+ }
191
+
192
+ // Get the transactional email template
193
+ $templateId = Mage::getStoreConfig('abandonedcartsconfig/options/email_template_sale');
194
+ // Get the sender
195
+ $sender = array();
196
+ $sender['email'] = Mage::getStoreConfig('abandonedcartsconfig/options/email');
197
+ $sender['name'] = Mage::getStoreConfig('abandonedcartsconfig/options/name');
198
+
199
+ // Send the emails via a loop
200
+ foreach ($recipients as $email => $recipient)
201
+ {
202
+ // Don't send the email if dryrun is set
203
+ if ($dryrun)
204
+ {
205
+ // Log data when dried run
206
+ Mage::helper('abandonedcarts')->log(sprintf("%s->%s", __METHOD__, print_r($recipient['emailTemplateVariables'],true)));
207
+ // If the test email is set and found
208
+ if (isset($testemail) && $email == $testemail)
209
+ {
210
+ Mage::helper('abandonedcarts')->log(__METHOD__ . "sendAbandonedCartsSaleEmail test: " . $email);
211
+ // Send the test email
212
+ Mage::getModel('core/email_template')
213
+ ->sendTransactional(
214
+ $templateId,
215
+ $sender,
216
+ $email,
217
+ $recipient['emailTemplateVariables']['fullname'] ,
218
+ $recipient['emailTemplateVariables'],
219
+ null);
220
+ }
221
+ }
222
+ else
223
+ {
224
+ Mage::helper('abandonedcarts')->log(__METHOD__ . "sendAbandonedCartsSaleEmail: " . $email);
225
+
226
+ // Send the email
227
+ Mage::getModel('core/email_template')
228
+ ->sendTransactional(
229
+ $templateId,
230
+ $sender,
231
+ $email,
232
+ $recipient['emailTemplateVariables']['fullname'] ,
233
+ $recipient['emailTemplateVariables'],
234
+ null);
235
+ }
236
+
237
+ // Load the quote
238
+ $quote = Mage::getModel('sales/quote')->load($recipient['cartId']);
239
+
240
+ // We change the notification attribute
241
+ $quote->setAbandonedSaleNotified(1);
242
+
243
+ // Save only if dryrun is false or if the test email is set and found
244
+ if (!$dryrun || (isset($testemail) && $email == $testemail))
245
+ {
246
+ $quote->save();
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+ catch (Exception $e)
255
+ {
256
+ Mage::helper('abandonedcarts')->log(__METHOD__ . " " . $e->getMessage());
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Send notification email to customer with abandoned carts after the number of days specified in the config
262
+ * @param boolean if dryrun is set to true, it won't send emails and won't alter quotes
263
+ * @param string email to test
264
+ */
265
+ public function sendAbandonedCartsEmail($nodate = false, $dryrun = false, $testemail = null)
266
+ {
267
+ if (Mage::helper('abandonedcarts')->getDryRun()) $dryrun = true;
268
+ if (Mage::helper('abandonedcarts')->getTestEmail()) $testemail = Mage::helper('abandonedcarts')->getTestEmail();
269
+
270
+ try
271
+ {
272
+ if (Mage::helper('abandonedcarts')->isEnabled())
273
+ {
274
+ // Date handling
275
+ $store = Mage_Core_Model_App::ADMIN_STORE_ID;
276
+ $timezone = Mage::app()->getStore($store)->getConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE);
277
+ date_default_timezone_set($timezone);
278
+
279
+ // If the nodate parameter is set to false
280
+ if (!$nodate)
281
+ {
282
+ // Get the delay provided and convert it to a proper date
283
+ $delay = Mage::getStoreConfig('abandonedcartsconfig/options/notify_delay');
284
+ $delay = date('Y-m-d H:i:s', time() - $delay * 24 * 3600);
285
+ }
286
+ else
287
+ {
288
+ // We create a date in the future to handle all abandoned carts
289
+ $delay = date('Y-m-d H:i:s', strtotime("+7 day"));
290
+ }
291
+
292
+ // Get the attribute id for the status attribute
293
+ $eavAttribute = new Mage_Eav_Model_Mysql4_Entity_Attribute();
294
+ $statusId = $eavAttribute->getIdByCode('catalog_product', 'status');
295
+
296
+ // Loop through the stores
297
+ foreach (Mage::app()->getWebsites() as $website) {
298
+ // Get the website id
299
+ $websiteId = $website->getWebsiteId();
300
+ foreach ($website->getGroups() as $group) {
301
+ $stores = $group->getStores();
302
+ foreach ($stores as $store) {
303
+
304
+ // Get the store id
305
+ $storeId = $store->getStoreId();
306
+ // Init the store to be able to load the quote and the collections properly
307
+ Mage::app()->init($storeId,'store');
308
+
309
+ // Get the product collection
310
+ $collection = Mage::getResourceModel('catalog/product_collection')->setStore($storeId);
311
+
312
+ // First collection: carts with products that became on sale
313
+ // Join the collection with the required tables
314
+ $collection->getSelect()
315
+ ->reset(Zend_Db_Select::COLUMNS)
316
+ ->columns(array('e.entity_id AS product_id',
317
+ 'e.sku',
318
+ 'catalog_flat.name as product_name',
319
+ 'catalog_flat.price as product_price',
320
+ 'quote_table.entity_id as cart_id',
321
+ 'quote_table.updated_at as cart_updated_at',
322
+ 'quote_table.abandoned_notified as has_been_notified',
323
+ 'quote_table.customer_email as customer_email',
324
+ 'quote_table.customer_firstname as customer_firstname',
325
+ 'quote_table.customer_lastname as customer_lastname'
326
+ )
327
+ )
328
+ ->joinInner(
329
+ array('quote_items' => 'sales_flat_quote_item'),
330
+ 'quote_items.product_id = e.entity_id AND quote_items.price > 0.00',
331
+ null)
332
+ ->joinInner(
333
+ array('quote_table' => 'sales_flat_quote'),
334
+ '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,
335
+ null)
336
+ ->joinInner(
337
+ array('catalog_flat' => 'catalog_product_flat_'.$storeId),
338
+ 'catalog_flat.entity_id = e.entity_id',
339
+ null)
340
+ ->joinInner(
341
+ array('catalog_enabled' => 'catalog_product_entity_int'),
342
+ 'catalog_enabled.entity_id = e.entity_id AND catalog_enabled.attribute_id = '.$statusId.' AND catalog_enabled.value = 1',
343
+ null)
344
+ ->joinInner(
345
+ array('inventory' => 'cataloginventory_stock_status'),
346
+ 'inventory.product_id = e.entity_id AND inventory.stock_status = 1 AND website_id = '.$websiteId,
347
+ null)
348
+ ->order('quote_table.updated_at DESC');
349
+
350
+ // echo $collection->printlogquery(true);
351
+ $collection->load();
352
+
353
+ // Recipients array
354
+ $recipients = array();
355
+
356
+ foreach($collection as $entry)
357
+ {
358
+ // Test if the customer is already in the array
359
+ if (!array_key_exists($entry->getCustomerEmail(), $recipients))
360
+ {
361
+ // Create an array of variables to assign to template
362
+ $emailTemplateVariables = array();
363
+
364
+ // Array that contains the data which will be used inside the template
365
+ $emailTemplateVariables['fullname'] = $entry->getCustomerFirstname().' '.$entry->getCustomerLastname();
366
+ $emailTemplateVariables['firstname'] = $entry->getCustomerFirstname();
367
+ $emailTemplateVariables['productname'] = $entry->getProductName();
368
+
369
+ // Assign the values to the array of recipients
370
+ $recipients[$entry->getCustomerEmail()]['cartId'] = $entry->getCartId();
371
+
372
+ $emailTemplateVariables['extraproductcount'] = 0;
373
+ }
374
+ else
375
+ {
376
+ // We create some extra variables if there is several products in the cart
377
+ $emailTemplateVariables = $recipients[$entry->getCustomerEmail()]['emailTemplateVariables'];
378
+ // We increase the product count
379
+ $emailTemplateVariables['extraproductcount'] += 1;
380
+ }
381
+ // Assign the array of template variables
382
+ $recipients[$entry->getCustomerEmail()]['emailTemplateVariables'] = $emailTemplateVariables;
383
+ }
384
+
385
+ // Get the transactional email template
386
+ $templateId = Mage::getStoreConfig('abandonedcartsconfig/options/email_template');
387
+ // Get the sender
388
+ $sender = array();
389
+ $sender['email'] = Mage::getStoreConfig('abandonedcartsconfig/options/email');
390
+ $sender['name'] = Mage::getStoreConfig('abandonedcartsconfig/options/name');
391
+
392
+ // Send the emails via a loop
393
+ foreach ($recipients as $email => $recipient)
394
+ {
395
+ // Don't send the email if dryrun is set
396
+ if ($dryrun)
397
+ {
398
+ // Log data when dried run
399
+ Mage::helper('abandonedcarts')->log(sprintf("%s->%s", __METHOD__, print_r($recipient['emailTemplateVariables'],true)));
400
+ // If the test email is set and found
401
+ if (isset($testemail) && $email == $testemail)
402
+ {
403
+ Mage::helper('abandonedcarts')->log(__METHOD__ . "sendAbandonedCartsEmail test: " . $email);
404
+ // Send the test email
405
+ Mage::getModel('core/email_template')
406
+ ->sendTransactional(
407
+ $templateId,
408
+ $sender,
409
+ $email,
410
+ $recipient['emailTemplateVariables']['fullname'] ,
411
+ $recipient['emailTemplateVariables'],
412
+ null);
413
+ }
414
+ }
415
+ else
416
+ {
417
+ Mage::helper('abandonedcarts')->log(__METHOD__ . "sendAbandonedCartsEmail: " . $email);
418
+
419
+ // Send the email
420
+ Mage::getModel('core/email_template')
421
+ ->sendTransactional(
422
+ $templateId,
423
+ $sender,
424
+ $email,
425
+ $recipient['emailTemplateVariables']['fullname'] ,
426
+ $recipient['emailTemplateVariables'],
427
+ null);
428
+ }
429
+
430
+ // Load the quote
431
+ $quote = Mage::getModel('sales/quote')->load($recipient['cartId']);
432
+
433
+ // We change the notification attribute
434
+ $quote->setAbandonedNotified(1);
435
+
436
+ // Save only if dryrun is false or if the test email is set and found
437
+ if (!$dryrun || (isset($testemail) && $email == $testemail))
438
+ {
439
+ $quote->save();
440
+ }
441
+ }
442
+ }
443
+ }
444
+ }
445
+ }
446
+ }
447
+ catch (Exception $e)
448
+ {
449
+ Mage::helper('abandonedcarts')->log(__METHOD__ . " " . $e->getMessage());
450
+ }
451
+ }
452
+ }
app/code/community/DigitalPianism/Abandonedcarts/controllers/Adminhtml/AbandonedcartsController.php ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class DigitalPianism_Abandonedcarts_Adminhtml_AbandonedcartsController extends Mage_Adminhtml_Controller_Action
4
+ {
5
+ /**
6
+ * Manually send the notifications
7
+ *
8
+ * @return void
9
+ */
10
+ public function sendAction()
11
+ {
12
+ $model = Mage::getModel('abandonedcarts/observer');
13
+ $model->sendAbandonedCartsEmail(true);
14
+ $model->sendAbandonedCartsSaleEmail();
15
+
16
+ $result = 1;
17
+ Mage::app()->getResponse()->setBody($result);
18
+ }
19
+ }
app/code/community/DigitalPianism/Abandonedcarts/etc/adminhtml.xml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <acl>
4
+ <resources>
5
+ <all>
6
+ <title>Allow Everything</title>
7
+ </all>
8
+ <admin>
9
+ <children>
10
+ <system>
11
+ <children>
12
+ <config>
13
+ <children>
14
+ <abandonedcartsconfig translate="title">
15
+ <title>Abandoned Carts Emails</title>
16
+ <sort_order>100</sort_order>
17
+ </abandonedcartsconfig>
18
+ </children>
19
+ </config>
20
+ </children>
21
+ </system>
22
+ </children>
23
+ </admin>
24
+ </resources>
25
+ </acl>
26
+ </config>
app/code/community/DigitalPianism/Abandonedcarts/etc/config.xml ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+
3
+ <config>
4
+
5
+ <modules>
6
+ <DigitalPianism_Abandonedcarts>
7
+ <version>0.1.3</version>
8
+ </DigitalPianism_Abandonedcarts>
9
+ </modules>
10
+
11
+ <admin>
12
+ <routers>
13
+
14
+ <abandonedcarts_admin>
15
+ <use>admin</use>
16
+ <args>
17
+ <module>DigitalPianism_Abandonedcarts</module>
18
+ <frontName>abandonedcarts_admin</frontName>
19
+ </args>
20
+ </abandonedcarts_admin>
21
+
22
+ </routers>
23
+ </admin>
24
+
25
+ <frontend>
26
+ <!-- Translation file -->
27
+ <translate>
28
+ <modules>
29
+ <DigitalPianism_Abandonedcarts>
30
+ <files>
31
+ <default>DigitalPianism_Abandonedcarts.csv</default>
32
+ </files>
33
+ </DigitalPianism_Abandonedcarts>
34
+ </modules>
35
+ </translate>
36
+ </frontend>
37
+
38
+ <adminhtml>
39
+ <!-- Backend translation file -->
40
+ <translate>
41
+ <modules>
42
+ <DigitalPianism_Abandonedcarts>
43
+ <files>
44
+ <default>DigitalPianism_Abandonedcarts.csv</default>
45
+ </files>
46
+ </DigitalPianism_Abandonedcarts>
47
+ </modules>
48
+ </translate>
49
+ </adminhtml>
50
+
51
+ <global>
52
+ <blocks>
53
+ <abandonedcarts>
54
+ <class>DigitalPianism_Abandonedcarts_Block</class>
55
+ </abandonedcarts>
56
+ </blocks>
57
+
58
+ <helpers>
59
+ <abandonedcarts>
60
+ <class>DigitalPianism_Abandonedcarts_Helper</class>
61
+ </abandonedcarts>
62
+ </helpers>
63
+
64
+ <models>
65
+ <abandonedcarts>
66
+ <class>DigitalPianism_Abandonedcarts_Model</class>
67
+ </abandonedcarts>
68
+ </models>
69
+
70
+ <template>
71
+ <email>
72
+ <sales_abandonedcarts_email_template translate="label" module="abandonedcarts">
73
+ <label>Abandoned Cart Template</label>
74
+ <file>digitalpianism/sales_abandonedcarts.html</file>
75
+ <type>html</type>
76
+ </sales_abandonedcarts_email_template>
77
+ <sales_abandonedcarts_email_template_sale translate="label" module="abandonedcarts">
78
+ <label>Abandoned Cart Sale Template</label>
79
+ <file>digitalpianism/sales_abandonedcarts_sale.html</file>
80
+ <type>html</type>
81
+ </sales_abandonedcarts_email_template_sale>
82
+ </email>
83
+ </template>
84
+
85
+ <resources>
86
+ <abandonedcarts_setup>
87
+ <setup>
88
+ <module>DigitalPianism_Abandonedcarts</module>
89
+ </setup>
90
+ <connection>
91
+ <use>core_setup</use>
92
+ </connection>
93
+ </abandonedcarts_setup>
94
+ <abandonedcarts_write>
95
+ <connection>
96
+ <use>core_write</use>
97
+ </connection>
98
+ </abandonedcarts_write>
99
+ <abandonedcarts_read>
100
+ <connection>
101
+ <use>core_read</use>
102
+ </connection>
103
+ </abandonedcarts_read>
104
+ </resources>
105
+
106
+ </global>
107
+
108
+ <crontab>
109
+ <jobs>
110
+ <digitalpianism_abandonedcarts_send>
111
+ <schedule>
112
+ <cron_expr>0 0 * * *</cron_expr>
113
+ </schedule>
114
+ <run>
115
+ <model>abandonedcarts/observer::sendAbandonedCartsEmail</model>
116
+ <model>abandonedcarts/observer::sendAbandonedCartsSaleEmail</model>
117
+ </run>
118
+ </digitalpianism_abandonedcarts_send>
119
+ </jobs>
120
+ </crontab>
121
+
122
+ <default>
123
+ <sales>
124
+ <abandonedcarts>
125
+ <notify_delay>20</notify_delay>
126
+ </abandonedcarts>
127
+ </sales>
128
+ </default>
129
+
130
+ </config>
app/code/community/DigitalPianism/Abandonedcarts/etc/system.xml ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <tabs>
4
+ <digitalpianism translate="label" module="abandonedcarts">
5
+ <label>Digital Pianism</label>
6
+ <sort_order>101</sort_order>
7
+ </digitalpianism>
8
+ </tabs>
9
+ <sections>
10
+ <abandonedcartsconfig translate="label" module="abandonedcarts">
11
+ <label>Abandoned Carts Emails</label>
12
+ <tab>digitalpianism</tab>
13
+ <sort_order>1000</sort_order>
14
+ <show_in_default>1</show_in_default>
15
+ <show_in_website>1</show_in_website>
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>
25
+ <fields>
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
+ <name translate="label">
36
+ <label>Sender Name</label>
37
+ <frontend_type>text</frontend_type>
38
+ <backend_model>adminhtml/system_config_backend_email_sender</backend_model>
39
+ <validate>validate-emailSender</validate>
40
+ <sort_order>20</sort_order>
41
+ <show_in_default>1</show_in_default>
42
+ <show_in_website>1</show_in_website>
43
+ <show_in_store>1</show_in_store>
44
+ </name>
45
+ <email translate="label">
46
+ <label>Sender Email</label>
47
+ <frontend_type>text</frontend_type>
48
+ <validate>validate-email</validate>
49
+ <backend_model>adminhtml/system_config_backend_email_address</backend_model>
50
+ <sort_order>30</sort_order>
51
+ <show_in_default>1</show_in_default>
52
+ <show_in_website>1</show_in_website>
53
+ <show_in_store>1</show_in_store>
54
+ </email>
55
+ <email_template translate="label">
56
+ <label>Email Template for Unaltered Abandoned Carts</label>
57
+ <frontend_type>select</frontend_type>
58
+ <source_model>adminhtml/system_config_source_email_template</source_model>
59
+ <sort_order>40</sort_order>
60
+ <show_in_default>1</show_in_default>
61
+ <show_in_website>1</show_in_website>
62
+ <show_in_store>1</show_in_store>
63
+ </email_template>
64
+ <notify_delay translate="label">
65
+ <label>Send Abandoned Cart Email After</label>
66
+ <frontend_type>text</frontend_type>
67
+ <validate>validate-not-negative-number</validate>
68
+ <sort_order>50</sort_order>
69
+ <show_in_default>1</show_in_default>
70
+ <show_in_website>1</show_in_website>
71
+ <show_in_store>1</show_in_store>
72
+ <comment>(days). NB: this only affects unaltered abandoned carts. If products go on sale, the email below is sent on the same day.</comment>
73
+ </notify_delay>
74
+ <enable_sale translate="label">
75
+ <label>Enable Sale Abandoned Carts Notification</label>
76
+ <frontend_type>select</frontend_type>
77
+ <source_model>adminhtml/system_config_source_yesno</source_model>
78
+ <sort_order>60</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
+ </enable_sale>
83
+ <email_template_sale translate="label">
84
+ <label>Email Template for Abandoned Carts Sale</label>
85
+ <frontend_type>select</frontend_type>
86
+ <source_model>adminhtml/system_config_source_email_template</source_model>
87
+ <sort_order>70</sort_order>
88
+ <show_in_default>1</show_in_default>
89
+ <show_in_website>1</show_in_website>
90
+ <show_in_store>1</show_in_store>
91
+ </email_template_sale>
92
+ <dryrun translate="label">
93
+ <label>Dry Run</label>
94
+ <frontend_type>select</frontend_type>
95
+ <source_model>adminhtml/system_config_source_yesno</source_model>
96
+ <sort_order>80</sort_order>
97
+ <show_in_default>1</show_in_default>
98
+ <show_in_website>1</show_in_website>
99
+ <show_in_store>1</show_in_store>
100
+ <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>
101
+ </dryrun>
102
+ <testemail translate="label">
103
+ <label>Test Email</label>
104
+ <frontend_type>text</frontend_type>
105
+ <validate>validate-email</validate>
106
+ <backend_model>adminhtml/system_config_backend_email_address</backend_model>
107
+ <sort_order>90</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
+ <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>
112
+ </testemail>
113
+ <send translate="label">
114
+ <label>Send Notifications Now</label>
115
+ <frontend_type>button</frontend_type>
116
+ <frontend_model>abandonedcarts/adminhtml_system_config_form_button</frontend_model>
117
+ <sort_order>100</sort_order>
118
+ <show_in_default>1</show_in_default>
119
+ <show_in_website>1</show_in_website>
120
+ <show_in_store>1</show_in_store>
121
+ </send>
122
+ </fields>
123
+ </options>
124
+ </groups>
125
+ </abandonedcartsconfig>
126
+ </sections>
127
+ </config>
app/code/community/DigitalPianism/Abandonedcarts/sql/abandonedcarts_setup/install-0.0.1.php ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $installer = $this;
4
+ $installer->startSetup();
5
+
6
+ // Add a notification column to the sales_flat_quote table
7
+ $installer
8
+ ->getConnection()
9
+ ->addColumn(
10
+ $this->getTable('sales/quote'), 'abandoned_notified', 'tinyint(1) not null default 0'
11
+ );
12
+
13
+ $installer->endSetup();
app/code/community/DigitalPianism/Abandonedcarts/sql/abandonedcarts_setup/upgrade-0.0.1-0.0.2.php ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $installer = $this;
4
+ $installer->startSetup();
5
+
6
+ // Add a notification column to the sales_flat_quote table
7
+ $installer
8
+ ->getConnection()
9
+ ->addColumn(
10
+ $this->getTable('sales/quote'), 'abandoned_sale_notified', 'tinyint(1) not null default 0'
11
+ );
12
+
13
+ $installer->endSetup();
app/design/adminhtml/default/default/template/digitalpianism/abandonedcarts/system/config/button.phtml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/etc/modules/DigitalPianism_Abandonedcarts.xml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <config>
3
+ <modules>
4
+ <DigitalPianism_Abandonedcarts>
5
+ <active>true</active>
6
+ <codePool>community</codePool>
7
+ </DigitalPianism_Abandonedcarts>
8
+ </modules>
9
+ </config>
app/locale/en_US/DigitalPianism_AbandonedCarts.csv ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ Abandoned Carts Email,Abandoned Carts Email
2
+ Sender Name,Sender Name
3
+ Sender Email,Sender Email
4
+ Email Template for Unaltered Abandoned Carts,Email Template for Unaltered Abandoned Carts
5
+ Send Abandoned Cart Email After,Send Abandoned Cart Email After
6
+ (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.
7
+ Email Template for Sale Products,Email Template for Sale Products
app/locale/en_US/template/email/digitalpianism/sales_abandonedcarts.html ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--@subject {{var fullname}} Abandoned Products In Your Shopping Bag! @-->
2
+ <body style="background:#F6F6F6; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:12px; margin:0; padding:0;">
3
+ <div style="background:#F6F6F6; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:12px; margin:0; padding:0;">
4
+ <table cellspacing="0" cellpadding="0" border="0" width="100%">
5
+ <tr>
6
+ <td align="center" valign="top" style="padding:20px 0 20px 0">
7
+ <table bgcolor="#FFFFFF" cellspacing="0" cellpadding="10" border="0" width="650" style="border:1px solid #E0E0E0;">
8
+ <!-- [ header starts here] -->
9
+ <tr>
10
+ <td valign="top"><a href="{{store url=""}}"><img src="{{var logo_url}}" alt="{{var logo_alt}}" style="margin-bottom:10px;" border="0"/></a></td>
11
+ </tr>
12
+ <!-- [ middle starts here] -->
13
+ <tr>
14
+ <td valign="top">
15
+ <p style="font-size:12px; line-height:16px; margin:0;">
16
+ Dear {{var firstname}},<br />
17
+ You have abandoned a {{var productname}} {{depend extraproductcount}} and {{var extraproductcount}} more products{{/depend}} in your cart.<br/>
18
+
19
+ Follow this link and log in to finalize your purchase: {{config path="web/unsecure/base_url"}}
20
+ </p>
21
+ </td>
22
+ </tr>
23
+ </table>
24
+ </td>
25
+ </tr>
26
+ </table>
27
+ </div>
28
+ </body>
app/locale/en_US/template/email/digitalpianism/sales_abandonedcarts_sale.html ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!--@subject {{var fullname}} Abandoned Sale Products In Your Shopping Bag! @-->
2
+ <body style="background:#F6F6F6; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:12px; margin:0; padding:0;">
3
+ <div style="background:#F6F6F6; font-family:Verdana, Arial, Helvetica, sans-serif; font-size:12px; margin:0; padding:0;">
4
+ <table cellspacing="0" cellpadding="0" border="0" width="100%">
5
+ <tr>
6
+ <td align="center" valign="top" style="padding:20px 0 20px 0">
7
+ <table bgcolor="#FFFFFF" cellspacing="0" cellpadding="10" border="0" width="650" style="border:1px solid #E0E0E0;">
8
+ <!-- [ header starts here] -->
9
+ <tr>
10
+ <td valign="top"><a href="{{store url=""}}"><img src="{{var logo_url}}" alt="{{var logo_alt}}" style="margin-bottom:10px;" border="0"/></a></td>
11
+ </tr>
12
+ <!-- [ middle starts here] -->
13
+ <tr>
14
+ <td valign="top">
15
+ <p style="font-size:12px; line-height:16px; margin:0;">
16
+ Dear {{var firstname}},<br />
17
+ You have abandoned a {{var productname}} in your cart.<br/>
18
+
19
+ It was {{var cartprice}} and now is {{var specialprice}}.<br/>
20
+
21
+ {{depend extraproductcount}}
22
+ Purchase the {{var extraproductcount}} other sale products in your cart and save {{var discount}} on your order.<br/>
23
+ {{/depend}}
24
+
25
+ Follow this link and log in to finalize your purchase with the new special price: {{config path="web/unsecure/base_url"}}
26
+ </p>
27
+ </td>
28
+ </tr>
29
+ </table>
30
+ </td>
31
+ </tr>
32
+ </table>
33
+ </div>
34
+ </body>
package.xml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0"?>
2
+ <package>
3
+ <name>DigitalPianism_Abandonedcarts</name>
4
+ <version>0.1.3</version>
5
+ <stability>stable</stability>
6
+ <license uri="http://opensource.org/licenses/osl-3.0.php">OSL v3.0</license>
7
+ <channel>community</channel>
8
+ <extends/>
9
+ <summary>Allows administrators to notice customers with abandoned carts.</summary>
10
+ <description>This extension allows store managers to automatically notice customers with abandoned carts after a customizable number of days via email.&#xD;
11
+ &#xD;
12
+ Another email can also be sent if one of the abandoned products goes on sale.</description>
13
+ <notes>- Add test email variable</notes>
14
+ <authors><author><name>Digital Pianism</name><user>digitalpianism</user><email>contact@digital-pianism.com</email></author></authors>
15
+ <date>2014-05-14</date>
16
+ <time>14:20:56</time>
17
+ <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="a27ec4c5bc4cb4e954ebdfebc71cbfd2"/></dir></dir></dir></dir></dir><dir name="Helper"><file name="Data.php" hash="c76d4abfe24cdad55badea279e13a9a1"/></dir><dir name="Model"><file name="Observer.php" hash="b95ab603a807473801a102ed917641b8"/></dir><dir name="controllers"><dir name="Adminhtml"><file name="AbandonedcartsController.php" hash="dc840a6de8c229175000df65b0d85289"/></dir></dir><dir name="etc"><file name="adminhtml.xml" hash="8ddca513c0ed7e034c476f3e026ceda8"/><file name="config.xml" hash="6f719a900407feb3dfd5f45bf931dc63"/><file name="system.xml" hash="54d83991ce27de6a37ba63083112817f"/></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="magelocale"><dir name="en_US"><file name="DigitalPianism_AbandonedCarts.csv" hash="4e17b6cae58dd1cdcd43b1113e2e09f4"/><dir name="template"><dir name="email"><dir name="digitalpianism"><file name="sales_abandonedcarts.html" hash="30565f91c47913465fd184a214c14b23"/><file name="sales_abandonedcarts_sale.html" hash="3cdee557727cb0166741062e5fdcf06f"/></dir></dir></dir></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></contents>
18
+ <compatible/>
19
+ <dependencies><required><php><min>4.1.0</min><max>6.0.0</max></php></required></dependencies>
20
+ </package>