nypwidget - Version 3.0.0

Version Notes

Adds support for checkout via Magento.

Download this release

Release Info

Developer PriceWaiter
Extension nypwidget
Version 3.0.0
Comparing to
See all releases


Code changes from version 2.5.5 to 3.0.0

Files changed (51) hide show
  1. app/code/community/PriceWaiter/NYPWidget/Block/Widget.php +19 -7
  2. app/code/community/PriceWaiter/NYPWidget/Controller/Endpoint.php +206 -0
  3. app/code/community/PriceWaiter/NYPWidget/Controller/Endpoint/Request.php +116 -0
  4. app/code/community/PriceWaiter/NYPWidget/Controller/Endpoint/Response.php +83 -0
  5. app/code/community/PriceWaiter/NYPWidget/Exception/Abstract.php +60 -0
  6. app/code/community/PriceWaiter/NYPWidget/Exception/DealAlreadyCreated.php +10 -0
  7. app/code/community/PriceWaiter/NYPWidget/Exception/DealAlreadyRevoked.php +10 -0
  8. app/code/community/PriceWaiter/NYPWidget/Exception/DealNotFound.php +10 -0
  9. app/code/community/PriceWaiter/NYPWidget/Exception/InvalidRegion.php +1 -1
  10. app/code/community/PriceWaiter/NYPWidget/Exception/NoTestDeals.php +10 -0
  11. app/code/community/PriceWaiter/NYPWidget/Exception/OrderNotFound.php +11 -0
  12. app/code/community/PriceWaiter/NYPWidget/Exception/OutOfStock.php +0 -10
  13. app/code/community/PriceWaiter/NYPWidget/Exception/Product/Abstract.php +1 -0
  14. app/code/community/PriceWaiter/NYPWidget/Exception/Product/Invalid.php +15 -0
  15. app/code/community/PriceWaiter/NYPWidget/Exception/Product/NotFound.php +1 -2
  16. app/code/community/PriceWaiter/NYPWidget/Exception/Product/OutOfStock.php +10 -0
  17. app/code/community/PriceWaiter/NYPWidget/Exception/Signature.php +1 -1
  18. app/code/community/PriceWaiter/NYPWidget/Exception/SingleItemOnly.php +10 -0
  19. app/code/community/PriceWaiter/NYPWidget/Exception/Version.php +34 -0
  20. app/code/community/PriceWaiter/NYPWidget/Helper/About.php +65 -0
  21. app/code/community/PriceWaiter/NYPWidget/Helper/Data.php +172 -360
  22. app/code/community/PriceWaiter/NYPWidget/Helper/Product.php +0 -108
  23. app/code/community/PriceWaiter/NYPWidget/Model/Callback.php +151 -1
  24. app/code/community/PriceWaiter/NYPWidget/Model/Callback/Inventory.php +2 -7
  25. app/code/community/PriceWaiter/NYPWidget/Model/Deal.php +282 -0
  26. app/code/community/PriceWaiter/NYPWidget/Model/Discounter.php +195 -0
  27. app/code/community/PriceWaiter/NYPWidget/Model/Embed.php +520 -0
  28. app/code/community/PriceWaiter/NYPWidget/Model/Mysql4/Deal.php +16 -0
  29. app/code/community/PriceWaiter/NYPWidget/Model/Mysql4/Deal/Collection.php +10 -0
  30. app/code/community/PriceWaiter/NYPWidget/Model/Mysql4/Deal/Usage.php +228 -0
  31. app/code/community/PriceWaiter/NYPWidget/Model/Observer.php +48 -0
  32. app/code/community/PriceWaiter/NYPWidget/Model/Offer/Item.php +501 -0
  33. app/code/community/PriceWaiter/NYPWidget/Model/Offer/Item/Handler.php +263 -0
  34. app/code/community/PriceWaiter/NYPWidget/Model/Offer/Item/Inventory.php +127 -0
  35. app/code/community/PriceWaiter/NYPWidget/Model/Offer/Item/Pricing.php +269 -0
  36. app/code/community/PriceWaiter/NYPWidget/Model/Session.php +112 -0
  37. app/code/community/PriceWaiter/NYPWidget/Model/Total/Quote.php +452 -0
  38. app/code/community/PriceWaiter/NYPWidget/controllers/Adminhtml/PricewaiterController.php +0 -14
  39. app/code/community/PriceWaiter/NYPWidget/controllers/CallbackController.php +1 -1
  40. app/code/community/PriceWaiter/NYPWidget/controllers/CheckoutController.php +204 -0
  41. app/code/community/PriceWaiter/NYPWidget/controllers/CreatedealController.php +46 -0
  42. app/code/community/PriceWaiter/NYPWidget/controllers/DebugController.php +77 -0
  43. app/code/community/PriceWaiter/NYPWidget/controllers/ListordersController.php +103 -0
  44. app/code/community/PriceWaiter/NYPWidget/controllers/PingController.php +22 -0
  45. app/code/community/PriceWaiter/NYPWidget/controllers/ProductinfoController.php +138 -38
  46. app/code/community/PriceWaiter/NYPWidget/controllers/RevokedealController.php +50 -0
  47. app/code/community/PriceWaiter/NYPWidget/etc/config.xml +45 -1
  48. app/code/community/PriceWaiter/NYPWidget/sql/nypwidget_setup/mysql4-install-1.0.0.php +1 -0
  49. app/code/community/PriceWaiter/NYPWidget/sql/nypwidget_setup/mysql4-upgrade-2.5.4-3.0.0.php +193 -0
  50. app/design/frontend/base/default/template/pricewaiter/widget.phtml +10 -79
  51. package.xml +6 -6
app/code/community/PriceWaiter/NYPWidget/Block/Widget.php CHANGED
@@ -2,14 +2,26 @@
2
 
3
  class PriceWaiter_NYPWidget_Block_Widget extends Mage_Core_Block_Template
4
  {
5
- public function _getHelper()
6
- {
7
- $helper = Mage::helper('nypwidget');
8
- return $helper;
9
- }
10
-
11
- public function getPriceWaiterOptions()
12
  {
 
13
  $product = Mage::registry('current_product');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
15
  }
2
 
3
  class PriceWaiter_NYPWidget_Block_Widget extends Mage_Core_Block_Template
4
  {
5
+ /**
6
+ * @return PriceWaiter_NYPWidget_Model_Embed
7
+ */
8
+ public function getEmbed()
 
 
 
9
  {
10
+ // Figure out where + who we are...
11
  $product = Mage::registry('current_product');
12
+ $store = Mage::app()->getStore();
13
+ $category = Mage::registry('current_category');
14
+
15
+ $session = Mage::getSingleton('customer/session');
16
+ $customer = $session->getCustomer();
17
+ $customerGroupId = $session->getCustomerGroupId();
18
+
19
+ // ..and wire up an appropriate embed.
20
+ return Mage::getModel('nypwidget/embed')
21
+ ->setProduct($product)
22
+ ->setStore($store)
23
+ ->setCategory($category)
24
+ ->setCustomer($customer->getId() ? $customer : false)
25
+ ->setCustomerGroupId($customerGroupId);
26
  }
27
  }
app/code/community/PriceWaiter/NYPWidget/Controller/Endpoint.php ADDED
@@ -0,0 +1,206 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ * Copyright 2013-2016 Price Waiter, LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ *
17
+ */
18
+
19
+ /**
20
+ * Base class for impelmenting PriceWaiter-specific endpoints.
21
+ * Our endpoints all use HTTP POST, with signed JSON request/response payloads.
22
+ * This base class handles the signing and request versioning.
23
+ */
24
+ abstract class PriceWaiter_NYPWidget_Controller_Endpoint extends Mage_Core_Controller_Front_Action
25
+ {
26
+ /**
27
+ * Set to the set of version numbers that this endpoint can process.
28
+ * @var array
29
+ */
30
+ protected $supportedVersions = [];
31
+
32
+ /**
33
+ * Content type in which responses are sent.
34
+ */
35
+ const RESPONSE_CONTENT_TYPE = 'application/json; charset=UTF-8';
36
+
37
+ /**
38
+ * HTTP header containing the API key of the PriceWaiter store.
39
+ */
40
+ const API_KEY_HEADER = 'X-PriceWaiter-Api-Key';
41
+
42
+ /**
43
+ * HTTP header containing the unique PW request id.
44
+ */
45
+ const REQUEST_ID_HEADER = 'X-PriceWaiter-Request-Id';
46
+
47
+ /**
48
+ * HTTP header in which request/response signature is stored.
49
+ */
50
+ const SIGNATURE_HEADER = 'X-PriceWaiter-Signature';
51
+
52
+ /**
53
+ * Header in which version is specified on requests.
54
+ */
55
+ const VERSION_HEADER = 'X-PriceWaiter-Version';
56
+
57
+
58
+ public function indexAction()
59
+ {
60
+ $httpRequest = Mage::app()->getRequest();
61
+ $httpResponse = Mage::app()->getResponse();
62
+
63
+ // We support POST only. For non-POST requests, we serve the traditional
64
+ // Magento "not found" page.
65
+ if (!$httpRequest->isPost()) {
66
+ return $this->silentNotFound($httpResponse);
67
+ }
68
+
69
+ $response = null;
70
+
71
+ try
72
+ {
73
+ $rawBody = $httpRequest->getRawBody();
74
+ $id = $httpRequest->getHeader(self::REQUEST_ID_HEADER);
75
+ $apiKey = $httpRequest->getHeader(self::API_KEY_HEADER);
76
+ $version = $httpRequest->getHeader(self::VERSION_HEADER);
77
+
78
+ $request = new PriceWaiter_NYPWidget_Controller_Endpoint_Request(
79
+ $id,
80
+ $apiKey,
81
+ $version,
82
+ $rawBody
83
+ );
84
+
85
+ $signature = $httpRequest->getHeader(self::SIGNATURE_HEADER);
86
+ $this->checkSignature($request, $signature);
87
+
88
+ $this->checkVersion($request);
89
+
90
+ $response = $this->processRequest($request);
91
+ }
92
+ catch (PriceWaiter_NYPWidget_Exception_Abstract $ex)
93
+ {
94
+ // This is an exception generated within our code, meant to be passed back to the client.
95
+ // It's reporting something like "signature verification failed" or "product could
96
+ // not be found" or "wtf is that currency".
97
+
98
+ $response = new PriceWaiter_NYPWidget_Controller_Endpoint_Response(
99
+ $ex->httpStatusCode,
100
+ array(
101
+ 'error' => $ex->jsonSerialize(),
102
+ )
103
+ );
104
+ }
105
+ catch (Exception $ex)
106
+ {
107
+ Mage::logException($ex);
108
+
109
+ $response = new PriceWaiter_NYPWidget_Controller_Endpoint_Response(
110
+ 500,
111
+ array(
112
+ 'error' => array(
113
+ 'code' => get_class($ex),
114
+ 'message' => $ex->getMessage(),
115
+ ),
116
+ )
117
+ );
118
+ }
119
+
120
+ $this->sendResponse($response, $httpResponse);
121
+ }
122
+
123
+ /**
124
+ * Override to handle a request to this endpoint.
125
+ * @param PriceWaiter_NYPWidget_Controller_Endpoint_Request $request
126
+ * @return Array Array in the format [ $httpStatusCode, $responseBody ]
127
+ */
128
+ abstract public function processRequest(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request);
129
+
130
+ /**
131
+ * Checks the X-PriceWaiter-Signature on the incoming request.
132
+ * @param Mage_Core_Controller_Request_Http $request
133
+ * @return Boolean
134
+ * @throws PriceWaiter_NYPWidget_Exception_Signature
135
+ */
136
+ protected function checkSignature(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request, $signature)
137
+ {
138
+ $secret = $this->getSharedSecret();
139
+
140
+ if ($request->isSignatureValid($signature, $secret)) {
141
+ return true;
142
+ }
143
+
144
+ throw new PriceWaiter_NYPWidget_Exception_Signature();
145
+ }
146
+
147
+ /**
148
+ * Checks the X-PriceWaiter-Version header on the incoming request.
149
+ * @param Mage_Core_Controller_Request_Http $request
150
+ * @return Boolean
151
+ * @throws PriceWaiter_NYPWidget_Exception_Version
152
+ */
153
+ protected function checkVersion(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request)
154
+ {
155
+ if (count($this->supportedVersions) === 0) {
156
+ $class = function_exists('get_called_class') ? get_called_class() : get_class();
157
+ throw new RuntimeException("$class does not specify $supportedVersions");
158
+ }
159
+
160
+ if (in_array($request->getVersion(), $this->supportedVersions)) {
161
+ return true;
162
+ }
163
+
164
+ throw new PriceWaiter_NYPWidget_Exception_Version($this->supportedVersions);
165
+ }
166
+
167
+ /**
168
+ * @return String The shared secret used for HMAC signing and verification.
169
+ */
170
+ protected function getSharedSecret()
171
+ {
172
+ $helper = Mage::helper('nypwidget');
173
+ return $helper->getSecret();
174
+ }
175
+ /**
176
+ * Writes a PriceWaiter Endpoint response object out using a standard Magento response.
177
+ * @param PriceWaiter_NYPWidget_Controller_Endpoint_Response $response
178
+ * @param Mage_Core_Controller_Response_Http $httpResponse
179
+ */
180
+ protected function sendResponse(PriceWaiter_NYPWidget_Controller_Endpoint_Response $response, Mage_Core_Controller_Response_Http $httpResponse)
181
+ {
182
+ $secret = $this->getSharedSecret();
183
+ $signature = $response->sign($secret);
184
+
185
+ $json = $response->getBodyJson();
186
+
187
+ $httpResponse->setHttpResponseCode($response->getStatusCode());
188
+ $httpResponse->setHeader('Content-type', self::RESPONSE_CONTENT_TYPE, true);
189
+ $httpResponse->setHeader(self::SIGNATURE_HEADER, $signature, true);
190
+
191
+ $about = Mage::helper('nypwidget/about');
192
+ $about->setResponseHeaders($httpResponse);
193
+
194
+ $httpResponse->setBody($json);
195
+ }
196
+
197
+ /**
198
+ * Returns a "not found" page as though this route did not even exist...
199
+ * @param Mage_Core_Controller_Response_Http $response
200
+ */
201
+ protected function silentNotFound(Mage_Core_Controller_Response_Http $httpResponse)
202
+ {
203
+ $httpResponse->setHttpResponseCode(404);
204
+ $this->norouteAction();
205
+ }
206
+ }
app/code/community/PriceWaiter/NYPWidget/Controller/Endpoint/Request.php ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ * Copyright 2013-2016 Price Waiter, LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ *
17
+ */
18
+
19
+ /**
20
+ * Simplified HTTP request abstraction for PriceWaiter endpoints.
21
+ */
22
+ class PriceWaiter_NYPWidget_Controller_Endpoint_Request
23
+ {
24
+ private $_apiKey;
25
+ private $_id;
26
+ private $_rawBody;
27
+ private $_version;
28
+ private $_timestamp;
29
+
30
+ public function __construct($id, $apiKey, $version, $rawBody, $timestamp = null)
31
+ {
32
+ $this->_id = $id;
33
+ $this->_apiKey = $apiKey;
34
+ $this->_version = $version;
35
+ $this->_rawBody = $rawBody;
36
+ $this->_timestamp = $timestamp === null ? time() : $timestamp;
37
+ }
38
+
39
+ /**
40
+ * @return String The public PriceWaiter API key for this request.
41
+ */
42
+ public function getApiKey()
43
+ {
44
+ return $this->_apiKey;
45
+ }
46
+
47
+ /**
48
+ * @return Object The request body.
49
+ */
50
+ public function getBody()
51
+ {
52
+ return json_decode($this->_rawBody);
53
+ }
54
+
55
+ /**
56
+ * @return String The unique PriceWaiter request id.
57
+ */
58
+ public function getId()
59
+ {
60
+ return $this->_id;
61
+ }
62
+
63
+ /**
64
+ * @return Integer UNIX timestamp representing request date/time.
65
+ */
66
+ public function getTimestamp()
67
+ {
68
+ return $this->_timestamp;
69
+ }
70
+
71
+ /**
72
+ * @return String Version of request data.
73
+ */
74
+ public function getVersion()
75
+ {
76
+ return $this->_version;
77
+ }
78
+
79
+ /**
80
+ * Checks an HMAC signature for this request.
81
+ * @param String $signature
82
+ * @param String $secret
83
+ * @return boolean
84
+ */
85
+ public function isSignatureValid($signature, $secret)
86
+ {
87
+ // NOTE: Never let an empty secret be used.
88
+ if (trim($secret) === '') {
89
+ return false;
90
+ }
91
+
92
+ $headers = array(
93
+ PriceWaiter_NYPWidget_Controller_Endpoint::API_KEY_HEADER => $this->getApiKey(),
94
+ PriceWaiter_NYPWidget_Controller_Endpoint::REQUEST_ID_HEADER => $this->getId(),
95
+ PriceWaiter_NYPWidget_Controller_Endpoint::VERSION_HEADER => $this->getVersion(),
96
+ );
97
+
98
+ $content = array();
99
+
100
+ foreach ($headers as $key => $value) {
101
+ $content[] = "${key}: $value";
102
+ }
103
+ $content[] = $this->_rawBody;
104
+ $content = implode("\n", $content);
105
+
106
+ $detected = 'sha256=' . hash_hmac('sha256', $content, $secret, false);
107
+
108
+ if (function_exists('hash_equals')) {
109
+ // Favor PHP's secure hash comparison function in 5.6 and up.
110
+ // For a robust drop-in compatibility shim, see: https://github.com/realityking/hash_equals
111
+ return hash_equals($detected, $signature);
112
+ }
113
+
114
+ return $detected === $signature;
115
+ }
116
+ }
app/code/community/PriceWaiter/NYPWidget/Controller/Endpoint/Response.php ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /*
3
+ * Copyright 2013-2016 Price Waiter, LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ *
17
+ */
18
+
19
+ /**
20
+ * A simplified abstract Response model for PriceWaiter HTTP endpoints.
21
+ */
22
+ class PriceWaiter_NYPWidget_Controller_Endpoint_Response
23
+ {
24
+ private $_body = null;
25
+ private $_statusCode = 200;
26
+ private $_bodyJson = null;
27
+
28
+ public function __construct($statusCode = 200, $body = [])
29
+ {
30
+ $this->_statusCode = $statusCode;
31
+ $this->_body = $body;
32
+ }
33
+
34
+ /**
35
+ * @return String JSON-encoded body.
36
+ */
37
+ public function getBodyJson()
38
+ {
39
+ if ($this->_bodyJson !== null) {
40
+ return $this->_bodyJson;
41
+ }
42
+
43
+ $options = 0;
44
+
45
+ if (defined('JSON_UNESCAPED_SLASHES')) {
46
+ $options |= JSON_UNESCAPED_SLASHES;
47
+ }
48
+
49
+ if (defined('JSON_PRETTY_PRINT')) {
50
+ $options |= JSON_PRETTY_PRINT;
51
+ }
52
+
53
+ return ($this->_bodyJson = json_encode($this->_body, $options));
54
+ }
55
+
56
+ /**
57
+ * @return Integer The HTTP status code to use.
58
+ */
59
+ public function getStatusCode()
60
+ {
61
+ return $this->_statusCode;
62
+ }
63
+
64
+ /**
65
+ * Returns the HMAC signature for this response.
66
+ * Meant for use in the X-PriceWaiter-Signature header.
67
+ * @param String $secret Shared secret.
68
+ * @return String.
69
+ */
70
+ public function sign($secret)
71
+ {
72
+ $json = $this->getBodyJson();
73
+ return 'sha256=' . hash_hmac('sha256', $this->getBodyJson(), $secret, false);
74
+ }
75
+
76
+ /**
77
+ * @return PriceWaiter_NYPWidget_Controller_Endpoint_Response A generic 200 "ok" response.
78
+ */
79
+ public static function ok()
80
+ {
81
+ return new self();
82
+ }
83
+ }
app/code/community/PriceWaiter/NYPWidget/Exception/Abstract.php CHANGED
@@ -18,6 +18,24 @@ abstract class PriceWaiter_NYPWidget_Exception_Abstract extends RuntimeException
18
  */
19
  public $httpStatusCode = 400;
20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  /**
22
  * @return Array Representation of this error, ready to be passed to json_encode.
23
  */
@@ -33,4 +51,46 @@ abstract class PriceWaiter_NYPWidget_Exception_Abstract extends RuntimeException
33
 
34
  return $json;
35
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
18
  */
19
  public $httpStatusCode = 400;
20
 
21
+ /**
22
+ * @internal
23
+ */
24
+ protected static $translations = array(
25
+ array(
26
+ 'class' => 'Mage_Core_Exception',
27
+ 'message' => 'This product is currently out of stock.',
28
+ 'helper' => 'cataloginventory',
29
+ 'translatedClass' => 'PriceWaiter_NYPWidget_Exception_Product_OutOfStock',
30
+ ),
31
+ array(
32
+ 'class' => 'Mage_Core_Exception',
33
+ 'message' => 'Not all products are available in the requested quantity',
34
+ 'helper' => 'cataloginventory',
35
+ 'translatedClass' => 'PriceWaiter_NYPWidget_Exception_Product_OutOfStock',
36
+ ),
37
+ );
38
+
39
  /**
40
  * @return Array Representation of this error, ready to be passed to json_encode.
41
  */
51
 
52
  return $json;
53
  }
54
+
55
+ /**
56
+ * @internal Attempts to convert a generic Magento exception into a typed PW one.
57
+ * @param Exception $ex
58
+ */
59
+ public static function translateMagentoException(Exception $ex)
60
+ {
61
+ foreach (self::$translations as $t) {
62
+ $translated = self::applyExceptionTranslation($ex, $t);
63
+ if ($translated) {
64
+ return $translated;
65
+ }
66
+ }
67
+
68
+ return $ex;
69
+ }
70
+
71
+ private static function applyExceptionTranslation(Exception $ex, array $t)
72
+ {
73
+ if (!($ex instanceof $t['class'])) {
74
+ return false;
75
+ }
76
+
77
+ $messages = array(
78
+ $t['message'],
79
+ );
80
+
81
+ if (!empty($t['helper'])) {
82
+ // Also check for a *translated* version of the message
83
+ $helper = Mage::helper($t['helper']);
84
+ $messages[] = $helper->__($t['message']);
85
+ }
86
+
87
+ $actualMessage = $ex->getMessage();
88
+ if (!in_array($actualMessage, $messages)) {
89
+ // No match
90
+ return false;
91
+ }
92
+
93
+ $translatedClass = $t['translatedClass'];
94
+ return new $translatedClass($actualMessage);
95
+ }
96
  }
app/code/community/PriceWaiter/NYPWidget/Exception/DealAlreadyCreated.php ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Exception thrown when a Deal cannot be created because it already was.
5
+ */
6
+ class PriceWaiter_NYPWidget_Exception_DealAlreadyCreated
7
+ extends PriceWaiter_NYPWidget_Exception_Abstract
8
+ {
9
+ public $errorCode = 'deal_already_exists';
10
+ }
app/code/community/PriceWaiter/NYPWidget/Exception/DealAlreadyRevoked.php ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Exception thrown when a Deal cannot be revoked because it already was.
5
+ */
6
+ class PriceWaiter_NYPWidget_Exception_DealAlreadyRevoked
7
+ extends PriceWaiter_NYPWidget_Exception_Abstract
8
+ {
9
+ public $errorCode = 'deal_already_revoked';
10
+ }
app/code/community/PriceWaiter/NYPWidget/Exception/DealNotFound.php ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Exception thrown when a Deal cannot be found by ID.
5
+ */
6
+ class PriceWaiter_NYPWidget_Exception_DealNotFound
7
+ extends PriceWaiter_NYPWidget_Exception_Abstract
8
+ {
9
+ public $errorCode = 'deal_not_found';
10
+ }
app/code/community/PriceWaiter/NYPWidget/Exception/InvalidRegion.php CHANGED
@@ -6,7 +6,7 @@
6
  class PriceWaiter_NYPWidget_Exception_InvalidRegion
7
  extends PriceWaiter_NYPWidget_Exception_Abstract
8
  {
9
- public $errorCode = 'invalid_region';
10
 
11
  /**
12
  * @var String
6
  class PriceWaiter_NYPWidget_Exception_InvalidRegion
7
  extends PriceWaiter_NYPWidget_Exception_Abstract
8
  {
9
+ public $errorCode = 'magento_invalid_region';
10
 
11
  /**
12
  * @var String
app/code/community/PriceWaiter/NYPWidget/Exception/NoTestDeals.php ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Exception thrown on any attempt to create a "test" deal.
5
+ */
6
+ class PriceWaiter_NYPWidget_Exception_NoTestDeals
7
+ extends PriceWaiter_NYPWidget_Exception_Abstract
8
+ {
9
+ public $errorCode = 'no_test_deals';
10
+ }
app/code/community/PriceWaiter/NYPWidget/Exception/OrderNotFound.php ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Exception thrown when an attempt is made to look up an order that
5
+ * does not exist.
6
+ */
7
+ class PriceWaiter_NYPWidget_Exception_OrderNotFound
8
+ extends PriceWaiter_NYPWidget_Exception_Abstract
9
+ {
10
+ public $errorCode = 'order_not_found';
11
+ }
app/code/community/PriceWaiter/NYPWidget/Exception/OutOfStock.php DELETED
@@ -1,10 +0,0 @@
1
- <?php
2
-
3
- /**
4
- * Exception thrown when there's not enough inventory of an item.
5
- */
6
- class PriceWaiter_NYPWidget_Exception_OutOfStock
7
- extends PriceWaiter_NYPWidget_Exception_Abstract
8
- {
9
- public $errorCode = 'out_of_stock';
10
- }
 
 
 
 
 
 
 
 
 
 
app/code/community/PriceWaiter/NYPWidget/Exception/Product/Abstract.php CHANGED
@@ -6,4 +6,5 @@
6
  abstract class PriceWaiter_NYPWidget_Exception_Product_Abstract
7
  extends PriceWaiter_NYPWidget_Exception_Abstract
8
  {
 
9
  }
6
  abstract class PriceWaiter_NYPWidget_Exception_Product_Abstract
7
  extends PriceWaiter_NYPWidget_Exception_Abstract
8
  {
9
+ public $errorCode = 'invalid_product';
10
  }
app/code/community/PriceWaiter/NYPWidget/Exception/Product/Invalid.php ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Exception thrown when an attempt is made to resolve an invalid product.
5
+ */
6
+ class PriceWaiter_NYPWidget_Exception_Product_Invalid
7
+ extends PriceWaiter_NYPWidget_Exception_Product_Abstract
8
+ {
9
+ const DEFAULT_MESSAGE = 'Invalid product data received.';
10
+
11
+ public function __construct($message = self::DEFAULT_MESSAGE)
12
+ {
13
+ parent::__construct($message);
14
+ }
15
+ }
app/code/community/PriceWaiter/NYPWidget/Exception/Product/NotFound.php CHANGED
@@ -1,10 +1,9 @@
1
  <?php
2
 
3
  /**
4
- * Exception thrown when a product cannot be found.
5
  */
6
  class PriceWaiter_NYPWidget_Exception_Product_NotFound
7
  extends PriceWaiter_NYPWidget_Exception_Product_Abstract
8
  {
9
- public $errorCode = 'product_not_found';
10
  }
1
  <?php
2
 
3
  /**
4
+ * Exception thrown when a product cannot be found by id.
5
  */
6
  class PriceWaiter_NYPWidget_Exception_Product_NotFound
7
  extends PriceWaiter_NYPWidget_Exception_Product_Abstract
8
  {
 
9
  }
app/code/community/PriceWaiter/NYPWidget/Exception/Product/OutOfStock.php ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Exception thrown when there's not enough inventory of an product.
5
+ */
6
+ class PriceWaiter_NYPWidget_Exception_Product_OutOfStock
7
+ extends PriceWaiter_NYPWidget_Exception_Product_Abstract
8
+ {
9
+ public $errorCode = 'out_of_stock';
10
+ }
app/code/community/PriceWaiter/NYPWidget/Exception/Signature.php CHANGED
@@ -7,5 +7,5 @@
7
  class PriceWaiter_NYPWidget_Exception_Signature
8
  extends PriceWaiter_NYPWidget_Exception_Abstract
9
  {
10
- public $errorCode = 'signature';
11
  }
7
  class PriceWaiter_NYPWidget_Exception_Signature
8
  extends PriceWaiter_NYPWidget_Exception_Abstract
9
  {
10
+ public $errorCode = 'invalid_signature';
11
  }
app/code/community/PriceWaiter/NYPWidget/Exception/SingleItemOnly.php ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Exception thrown to indicate this installation is only capable working with single-item deals.
5
+ */
6
+ class PriceWaiter_NYPWidget_Exception_SingleItemOnly
7
+ extends PriceWaiter_NYPWidget_Exception_Abstract
8
+ {
9
+ public $errorCode = 'single_item_only';
10
+ }
app/code/community/PriceWaiter/NYPWidget/Exception/Version.php ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Exception thrown when an incoming request is of a version that we
5
+ * don't support.
6
+ */
7
+ class PriceWaiter_NYPWidget_Exception_Version
8
+ extends PriceWaiter_NYPWidget_Exception_Abstract
9
+ {
10
+ public $errorCode = 'invalid_version';
11
+
12
+ /**
13
+ * Versions that we *do* support.
14
+ * @var Array
15
+ */
16
+ public $supportedVersions;
17
+
18
+ public function __construct(Array $supportedVersions)
19
+ {
20
+ $this->supportedVersions = $supportedVersions;
21
+
22
+ parent::__construct("Invalid version.");
23
+ }
24
+
25
+ public function jsonSerialize()
26
+ {
27
+ $json = parent::jsonSerialize();
28
+ $json['data'] = array(
29
+ 'supportedVersions' => $this->supportedVersions,
30
+ );
31
+
32
+ return $json;
33
+ }
34
+ }
app/code/community/PriceWaiter/NYPWidget/Helper/About.php ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Helper that provides metadata about the operating environment.
5
+ */
6
+ class PriceWaiter_NYPWidget_Helper_About extends Mage_Core_Helper_Abstract
7
+ {
8
+ /**
9
+ * HTTP header on response that tells PriceWaiter the platform version.
10
+ */
11
+ const EXTENSION_VERSION_HEADER = 'X-PriceWaiter-Extension-Version';
12
+
13
+ /**
14
+ * HTTP header on response that tells PriceWaiter the platform name.
15
+ */
16
+ const PLATFORM_HEADER = 'X-PriceWaiter-Platform';
17
+
18
+ /**
19
+ * HTTP header on response that tells PriceWaiter the platform version.
20
+ */
21
+ const PLATFORM_VERSION_HEADER = 'X-PriceWaiter-Platform-Version';
22
+
23
+ /**
24
+ * @return String PriceWaiter extension version.
25
+ */
26
+ public function getExtensionVersion()
27
+ {
28
+ try
29
+ {
30
+ return (string)Mage::getConfig()->getNode()->modules->PriceWaiter_NYPWidget->version;
31
+ }
32
+ catch (Exception $ex)
33
+ {
34
+ return 'unknown';
35
+ }
36
+ }
37
+
38
+ /**
39
+ * @return String Platform identification string.
40
+ */
41
+ public function getPlatform()
42
+ {
43
+ return 'Magento ' . Mage::getEdition();
44
+ }
45
+
46
+ /**
47
+ * @return String Magento version.
48
+ */
49
+ public function getPlatformVersion()
50
+ {
51
+ return Mage::getVersion();
52
+ }
53
+
54
+ /**
55
+ * @internal Adds "about" response headers.
56
+ * @param Zend_Controller_Response_Http $httpResponse
57
+ */
58
+ public function setResponseHeaders(Zend_Controller_Response_Http $httpResponse)
59
+ {
60
+ $httpResponse->setHeader(self::PLATFORM_HEADER, $this->getPlatform(), true);
61
+ $httpResponse->setHeader(self::PLATFORM_VERSION_HEADER, $this->getPlatformVersion(), true);
62
+ $httpResponse->setHeader(self::EXTENSION_VERSION_HEADER, $this->getExtensionVersion(), true);
63
+ }
64
+
65
+ }
app/code/community/PriceWaiter/NYPWidget/Helper/Data.php CHANGED
@@ -4,21 +4,17 @@ class PriceWaiter_NYPWidget_Helper_Data extends Mage_Core_Helper_Abstract
4
  {
5
  const PRICEWAITER_API_URL = 'https://api.pricewaiter.com';
6
  const PRICEWAITER_RETAILER_URL = 'https://retailer.pricewaiter.com';
 
7
 
 
 
 
8
  const XML_PATH_DEFAULT_ORDER_STATUS = 'pricewaiter/orders/default_status';
9
-
10
- private $_product = false;
11
- private $_buttonEnabled = null;
12
- private $_conversionToolsEnabled = null;
13
-
14
- private $_widgetUrl = 'https://widget.pricewaiter.com';
15
-
16
- public function __construct()
17
- {
18
- if (!!getenv('PRICEWAITER_WIDGET_URL')) {
19
- $this->_widgetUrl = getenv('PRICEWAITER_WIDGET_URL');
20
- }
21
- }
22
 
23
  /**
24
  * @return String URL of the PriceWaiter API.
@@ -74,128 +70,13 @@ class PriceWaiter_NYPWidget_Helper_Data extends Mage_Core_Helper_Abstract
74
  }
75
 
76
  /**
77
- * @return String The URL of the PriceWaiter Retailer area.
 
78
  */
79
- public function getRetailerUrl()
80
- {
81
- $url = getenv('PRICEWAITER_RETAILER_URL');
82
-
83
- if ($url) {
84
- return $url;
85
- }
86
-
87
- return self::PRICEWAITER_RETAILER_URL;
88
- }
89
-
90
- public function isEnabledForStore()
91
- {
92
- // Is the pricewaiter widget enabled for this store and an API Key has been set.
93
- if (Mage::getStoreConfig('pricewaiter/configuration/enabled')
94
- && Mage::getStoreConfig('pricewaiter/configuration/api_key')
95
- ) {
96
- return true;
97
- }
98
-
99
- return false;
100
- }
101
-
102
- // Set the values of $_buttonEnabled and $_conversionToolsEnabled
103
- private function _setEnabledStatus()
104
- {
105
- if ($this->_buttonEnabled != null && $this->_conversionToolsEnabled != null) {
106
- return true;
107
- }
108
-
109
- if (Mage::getStoreConfig('pricewaiter/configuration/enabled')) {
110
- $this->_buttonEnabled = true;
111
- }
112
-
113
- if (Mage::getStoreConfig('pricewaiter/conversion_tools/enabled')) {
114
- $this->_conversionToolsEnabled = true;
115
- }
116
-
117
- $product = $this->_getProduct();
118
-
119
- // Is the PriceWaiter widget enabled for this category
120
- $category = Mage::registry('current_category');
121
- if (is_object($category)) {
122
- $nypcategory = Mage::getModel('nypwidget/category')->loadByCategory($category);
123
- if (!$nypcategory->isActive()) {
124
- $this->_buttonEnabled = false;
125
- }
126
- if (!$nypcategory->isConversionToolsEnabled()) {
127
- $this->_conversionToolsEnabled = false;
128
- }
129
- } else {
130
- // We end up here if we are visiting the product page without being
131
- // "in a category". Basically, we arrived via a search page.
132
- // The logic here checks to see if there are any categories that this
133
- // product belongs to that enable the PriceWaiter widget. If not, return false.
134
- $categories = $product->getCategoryIds();
135
- $categoryActive = false;
136
- $categoryCTActive = false;
137
- foreach ($categories as $categoryId) {
138
- unset($currentCategory);
139
- unset($nypcategory);
140
- $currentCategory = Mage::getModel('catalog/category')->load($categoryId);
141
- $nypcategory = Mage::getModel('nypwidget/category')->loadByCategory($currentCategory);
142
- if ($nypcategory->isActive()) {
143
- if ($nypcategory->isConversionToolsEnabled()) {
144
- $categoryCTActive = true;
145
- }
146
- $categoryActive = true;
147
- break;
148
- }
149
- }
150
- if (!$categoryActive) {
151
- $this->_buttonEnabled = false;
152
- }
153
-
154
- if (!$categoryCTActive) {
155
- $this->_conversionToolsEnabled = false;
156
- }
157
-
158
- }
159
-
160
- // Is PriceWaiter enabled for this Customer Group
161
- $disable = Mage::getStoreConfig('pricewaiter/customer_groups/disable');
162
- if ($disable) {
163
- // An admin has chosen to disable the PriceWaiter widget by customer group.
164
- $customerGroupId = Mage::getSingleton('customer/session')->getCustomerGroupId();
165
- $customerGroups = Mage::getStoreConfig('pricewaiter/customer_groups/group_select');
166
- $customerGroups = preg_split('/,/', $customerGroups);
167
-
168
- if (in_array($customerGroupId, $customerGroups)) {
169
- $this->_buttonEnabled = false;
170
- }
171
- }
172
-
173
- // Are Conversion Tools enabled for this Customer Group
174
- $disableCT = Mage::getStoreConfig('pricewaiter/conversion_tools/customer_group_disable');
175
- if ($disableCT) {
176
- // An admin has chosen to disable the Conversion Tools by customer group.
177
- $customerGroupId = Mage::getSingleton('customer/session')->getCustomerGroupId();
178
- $customerGroups = Mage::getStoreConfig('pricewaiter/conversion_tools/group_select');
179
- $customerGroups = preg_split('/,/', $customerGroups);
180
-
181
- if (in_array($customerGroupId, $customerGroups)) {
182
- $this->_conversionToolsEnabled = false;
183
- }
184
- }
185
- }
186
-
187
- public function isConversionToolsEnabled()
188
- {
189
- $this->_setEnabledStatus();
190
-
191
- return $this->_conversionToolsEnabled;
192
- }
193
-
194
- public function isButtonEnabled()
195
  {
196
- $this->_setEnabledStatus();
197
-
198
- return $this->_buttonEnabled;
199
  }
200
 
201
  public function getPriceWaiterSettingsUrl()
@@ -203,102 +84,35 @@ class PriceWaiter_NYPWidget_Helper_Data extends Mage_Core_Helper_Abstract
203
  return $this->getRetailerUrl();
204
  }
205
 
206
- public function getWidgetUrl()
207
- {
208
- if ($this->isEnabledForStore()) {
209
- return $this->_widgetUrl . '/script/'
210
- . Mage::getStoreConfig('pricewaiter/configuration/api_key')
211
- . ".js";
212
- }
213
-
214
- return $this->_widgetUrl . '/nyp/script/widget.js';
215
- }
216
-
217
-
218
- public function getProductPrice($product)
219
  {
220
- $productPrice = 0;
221
-
222
- if ($product->getId()) {
223
- if ($product->getTypeId() != 'grouped') {
224
- $productPrice = $product->getFinalPrice();
225
- }
226
- }
227
-
228
- return $productPrice;
229
- }
230
-
231
- private function safeGetAttributeText($product, $code) {
232
- $value = $product->getData($code);
233
-
234
- // prevent Magento from rendering "No" when nothing is selected.
235
- if (!$value) {
236
- return false;
237
- }
238
-
239
- $resource = $product->getResource();
240
- if (!$resource) {
241
- return false;
242
- }
243
-
244
- $attr = $resource->getAttribute($code);
245
- if (!$attr) {
246
- return false;
247
- }
248
-
249
- $frontend = $attr->getFrontend();
250
- if (!$frontend) {
251
- return false;
252
- }
253
-
254
- return $frontend->getValue($product);
255
- }
256
-
257
- public function getProductBrand($product) {
258
-
259
- // prefer brand, but fallback to manufacturer attribute
260
- $brand = $product->getData('brand');
261
-
262
- if (!$brand) {
263
- $manufacturer = $this->safeGetAttributeText($product, 'manufacturer');
264
- if ($manufacturer) {
265
- $brand = $manufacturer;
266
- }
267
- }
268
 
269
- // try looking up popular plugin for brand attribute
270
- if (!$brand) {
271
- $manufacturer = $this->safeGetAttributeText($product, 'c2c_brand');
272
- if ($manufacturer) {
273
- $brand = $manufacturer;
274
- }
275
  }
276
 
277
- return $brand;
278
  }
279
 
280
- private function _getProduct()
 
 
 
 
281
  {
282
- if (!$this->_product) {
283
- $this->_product = Mage::registry('current_product');
284
- }
285
 
286
- return $this->_product;
287
- }
288
-
289
- public function getGroupedProductInfo()
290
- {
291
- $product = $this->_getProduct();
292
- $javascript = "var PriceWaiterGroupedProductInfo = new Array();\n";
293
-
294
- $associatedProducts = $product->getTypeInstance(true)->getAssociatedProducts($product);
295
- foreach ($associatedProducts as $simpleProduct) {
296
- $javascript .= "PriceWaiterGroupedProductInfo[" . $simpleProduct->getId() . "] = ";
297
- $javascript .= "new Array('" . htmlentities($simpleProduct->getName()) . "', '"
298
- . number_format($simpleProduct->getPrice(), 2) . "')\n";
299
  }
300
 
301
- return $javascript;
302
  }
303
 
304
  /**
@@ -312,9 +126,8 @@ class PriceWaiter_NYPWidget_Helper_Data extends Mage_Core_Helper_Abstract
312
  $stores = Mage::app()->getStores();
313
 
314
  foreach ($stores as $store) {
315
-
316
  $storeApiKey = Mage::getStoreConfig(
317
- 'pricewaiter/configuration/api_key',
318
  $store->getId()
319
  );
320
 
@@ -327,189 +140,188 @@ class PriceWaiter_NYPWidget_Helper_Data extends Mage_Core_Helper_Abstract
327
  }
328
 
329
  /**
330
- * Returns the secret token used when communicating with PriceWaiter.
331
- * @return {String} Secret token
332
  */
333
- public function getSecret()
334
  {
335
- $token = Mage::getStoreConfig('pricewaiter/configuration/api_secret');
336
 
337
- if (is_null($token) || $token == '') {
338
- $token = bin2hex(openssl_random_pseudo_bytes(24));
339
- $config = Mage::getModel('core/config');
340
 
341
- $config->saveConfig('pricewaiter/configuration/api_secret', $token);
 
 
 
 
342
  }
343
 
344
- return $token;
 
345
  }
346
 
347
  /**
348
- * Returns a signature that can be added to the head of a PriceWaiter API response.
349
- * @param {String} $responseBody The full body of the request to sign.
350
- * @return {String} Signature that should be set as the X-PriceWaiter-Signature header.
351
  */
352
- public function getResponseSignature($responseBody)
353
  {
354
- $signature = 'sha256=' . hash_hmac('sha256', $responseBody, $this->getSecret(), false);
355
- return $signature;
 
 
 
356
  }
357
 
358
  /**
359
- * Validates that the current request came from PriceWaiter.
360
- * @param {String} $signatureHeader Full value of the X-PriceWaiter-Signature header.
361
- * @param {String} $requestBody Complete body of incoming request.
362
- * @return {Boolean} Wehther the request actually came from PriceWaiter.
363
  */
364
- public function isPriceWaiterRequestValid($signatureHeader = null, $requestBody = null)
365
  {
366
- if ($signatureHeader === null || $requestBody === null) {
367
- return false;
368
- }
 
 
 
 
 
 
 
 
 
 
 
369
 
370
- $detected = 'sha256=' . hash_hmac('sha256', $requestBody, $this->getSecret(), false);
 
371
 
372
- if (function_exists('hash_equals')) {
373
- // Favor PHP's secure hash comparison function in 5.6 and up.
374
- // For a robust drop-in compatibility shim, see: https://github.com/indigophp/hash-compat
375
- return hash_equals($detected, $signatureHeader);
376
- }
 
 
 
 
 
 
 
 
377
 
378
- return $detected === $signatureHeader;
 
 
 
 
 
 
 
 
 
 
 
379
  }
380
 
381
  /**
382
- * Finds the Product that matches the given options and SKU
383
- * @param {String} $sku SKU of the product
384
- * @param {Array} $productOptions An array of options for the product, name => value
385
- * @return {Object} Returns Mage_Catalog_Model_Product of product that matches options.
386
- * @throws PriceWaiter_NYPWidget_Exception_Product_NotFound If no product can be found.
387
  */
388
- public function getProductWithOptions($sku, $productOptions)
389
  {
390
- $product = Mage::getModel('catalog/product')->getCollection()
391
- ->addAttributeToFilter('sku', $sku)
392
- ->addAttributeToSelect('*')
393
- ->getFirstItem();
394
-
395
- $additionalCost = null;
396
-
397
- if ($product->getTypeId() == 'configurable') {
398
- // Do configurable product specific stuff
399
- $attrs = $product->getTypeInstance(true)->getConfigurableAttributesAsArray($product);
400
-
401
- // Find our product based on attributes
402
- foreach ($attrs as $attr) {
403
- if (array_key_exists($attr['label'], $productOptions)) {
404
- foreach ($attr['values'] as $value) {
405
- if ($value['label'] == $productOptions[$attr['label']]) {
406
- $valueIndex = $value['value_index'];
407
- // If this attribute has a price assosciated with it, add it to the price later
408
- if ($value['pricing_value'] != '') {
409
- $additionalCost += $value['pricing_value'];
410
- }
411
- break;
412
- }
413
- }
414
- unset($productOptions[$attr['label']]);
415
- $productOptions[$attr['attribute_id']] = $valueIndex;
416
- }
417
- }
418
 
419
- $parentProduct = $product;
420
- $product = $product->getTypeInstance()->getProductByAttributes($productOptions, $product);
421
 
422
- if (!$product) {
423
- throw new PriceWaiter_NYPWidget_Exception_Product_NotFound();
424
- }
 
 
 
 
 
 
 
 
 
 
425
 
426
- $product->load($product->getId());
 
 
 
 
427
  }
428
 
429
- if ($additionalCost) {
430
- $product->setPrice($product->getPrice() + $additionalCost);
 
 
 
 
 
 
 
 
 
 
 
431
  }
432
 
433
- return $product;
434
- }
435
 
436
- public function getGroupedQuantity($productConfiguration)
437
- {
438
- $associatedProductIds = array_keys($productConfiguration['super_group']);
439
- $quantities = array();
440
- foreach ($associatedProductIds as $associatedProductId) {
441
- $associatedProduct = Mage::getModel('catalog/product')->load($associatedProductId);
442
- $quantities[] = $associatedProduct->getStockItem()->getQty();
443
  }
444
 
445
- return min($quantities);
446
  }
447
 
448
- public function getGroupedFinalPrice($productConfiguration)
 
 
 
 
 
 
 
 
 
 
449
  {
450
- $associatedProductIds = array_keys($productConfiguration['super_group']);
451
- $finalPrice = 0;
452
- foreach ($associatedProductIds as $associatedProductId) {
453
- $associatedProduct = Mage::getModel('catalog/product')->load($associatedProductId);
454
- $finalPrice += ($associatedProduct->getFinalPrice() * $productConfiguration['super_group'][$associatedProductId]);
455
- }
456
- return $finalPrice;
457
- }
458
 
459
- public function getGroupedCost($productConfiguration)
460
- {
461
- $associatedProductIds = array_keys($productConfiguration['super_group']);
462
- $costs = array();
463
- foreach ($associatedProductIds as $associatedProductId) {
464
- $associatedProduct = Mage::getModel('catalog/product')->load($associatedProductId);
465
- $costs[] = $associatedProduct->getData('cost');
466
  }
467
 
468
- return min($costs);
469
- }
 
470
 
471
- public function setHeaders()
472
- {
473
- $magentoEdition = 'Magento ' . Mage::getEdition();
474
- $magentoVersion = Mage::getVersion();
475
- $extensionVersion = Mage::getConfig()->getNode()->modules->PriceWaiter_NYPWidget->version;
476
- Mage::app()->getResponse()->setHeader('X-Platform', $magentoEdition, true);
477
- Mage::app()->getResponse()->setHeader('X-Platform-Version', $magentoVersion, true);
478
- Mage::app()->getResponse()->setHeader('X-Platform-Extension-Version', $extensionVersion, true);
479
-
480
- return true;
481
- }
482
 
483
- public function getCategoriesAsJSON($product)
484
- {
485
- $categorization = array();
486
- $assignedCategories = $product->getCategoryCollection()
487
- ->addAttributeToSelect('name');
488
-
489
- $baseUrl = Mage::app()->getStore()->getBaseUrl();
490
-
491
- // Find the path (parents) of each category, and add their information
492
- // to the categorization array
493
- foreach ($assignedCategories as $assignedCategory) {
494
- $parentCategories = array();
495
- $path = $assignedCategory->getPath();
496
- $parentIds = explode('/', $path);
497
- array_shift($parentIds); // We don't care about the root category
498
-
499
- $categoryModel = Mage::getModel('catalog/category');
500
- foreach($parentIds as $parentCategoryId) {
501
- $parentCategory = $categoryModel->load($parentCategoryId);
502
- $parentCategoryUrl = preg_replace('/^\//', '', $parentCategory->getUrlPath());
503
-
504
- $parentCategories[] = array(
505
- 'name' => $parentCategory->getName(),
506
- 'url' => $baseUrl . '/' . $parentCategoryUrl
507
- );
508
- }
509
 
510
- $categorization[] = $parentCategories;
 
511
  }
512
 
513
- return json_encode($categorization);
514
  }
515
  }
4
  {
5
  const PRICEWAITER_API_URL = 'https://api.pricewaiter.com';
6
  const PRICEWAITER_RETAILER_URL = 'https://retailer.pricewaiter.com';
7
+ const PRICEWAITER_WIDGET_URL = 'https://widget.pricewaiter.com';
8
 
9
+ const XML_PATH_API_KEY = 'pricewaiter/configuration/api_key';
10
+ const XML_PATH_BUTTON_ENABLED = 'pricewaiter/configuration/enabled';
11
+ const XML_PATH_CONVERSION_TOOLS_ENABLED = 'pricewaiter/conversion_tools/enabled';
12
  const XML_PATH_DEFAULT_ORDER_STATUS = 'pricewaiter/orders/default_status';
13
+ const XML_PATH_DISABLE_BY_CUSTOMER_GROUP = 'pricewaiter/customer_groups/disable';
14
+ const XML_PATH_BUTTON_DISABLED_CUSTOMER_GROUPS = 'pricewaiter/customer_groups/group_select';
15
+ const XML_PATH_CONVERSION_TOOLS_DISABLED_CUSTOMER_GROUPS = 'pricewaiter/conversion_tools/customer_group_disable';
16
+ const XML_PATH_DISABLED_BY_CATEGORY = 'pricewaiter/categories/disable_by_category';
17
+ const XML_PATH_SECRET = 'pricewaiter/configuration/api_secret';
 
 
 
 
 
 
 
 
18
 
19
  /**
20
  * @return String URL of the PriceWaiter API.
70
  }
71
 
72
  /**
73
+ * @param Mage_Core_Model_Store|number|null $store
74
+ * @return String|false API key or false if not configured.
75
  */
76
+ public function getPriceWaiterApiKey($store = null)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  {
78
+ $apiKey = Mage::getStoreConfig(self::XML_PATH_API_KEY, $store);
79
+ return $apiKey ? (string)$apiKey : false;
 
80
  }
81
 
82
  public function getPriceWaiterSettingsUrl()
84
  return $this->getRetailerUrl();
85
  }
86
 
87
+ /**
88
+ * @return String The URL of the PriceWaiter Retailer area.
89
+ */
90
+ public function getRetailerUrl()
 
 
 
 
 
 
 
 
 
91
  {
92
+ $url = getenv('PRICEWAITER_RETAILER_URL');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ if ($url) {
95
+ return $url;
 
 
 
 
96
  }
97
 
98
+ return self::PRICEWAITER_RETAILER_URL;
99
  }
100
 
101
+ /**
102
+ * Returns the secret token used when communicating with PriceWaiter.
103
+ * @return {String} Secret token
104
+ */
105
+ public function getSecret($store = null)
106
  {
107
+ $token = Mage::getStoreConfig(self::XML_PATH_SECRET, $store);
 
 
108
 
109
+ if (is_null($token) || $token == '') {
110
+ $token = bin2hex(openssl_random_pseudo_bytes(24));
111
+ $config = Mage::getModel('core/config');
112
+ $config->saveConfig(self::XML_PATH_SECRET, $token);
 
 
 
 
 
 
 
 
 
113
  }
114
 
115
+ return $token;
116
  }
117
 
118
  /**
126
  $stores = Mage::app()->getStores();
127
 
128
  foreach ($stores as $store) {
 
129
  $storeApiKey = Mage::getStoreConfig(
130
+ self::XML_PATH_API_KEY,
131
  $store->getId()
132
  );
133
 
140
  }
141
 
142
  /**
143
+ * @param Mage_Core_Model_Store|number|null $store
144
+ * @return String|false The widget.js URL, or false if not available.
145
  */
146
+ public function getWidgetJsUrl($store = null)
147
  {
148
+ $apiKey = $this->getPriceWaiterApiKey($store);
149
 
150
+ if (!$apiKey) {
151
+ return false;
152
+ }
153
 
154
+ // Allow overriding widget js url via ENV
155
+ $url = getenv('PRICEWAITER_WIDGET_URL');
156
+
157
+ if (!$url) {
158
+ $url = self::PRICEWAITER_WIDGET_URL;
159
  }
160
 
161
+ $apiKey = rawurlencode($apiKey);
162
+ return "{$url}/script/$apiKey.js";
163
  }
164
 
165
  /**
166
+ * @param array $categories An array of categories (or category ids).
167
+ * @param Mage_Core_Model_Store|number|null $store
168
+ * @return boolean Whether the button is enabled for *at least one* of the given categories.
169
  */
170
+ public function isButtonEnabledForAnyCategory(array $categories, $store = null)
171
  {
172
+ return $this->isFeatureEnabledForAnyCategory(
173
+ $categories,
174
+ $store,
175
+ 'isActive' // e.g. $nypcategory->isActive()
176
+ );
177
  }
178
 
179
  /**
180
+ * @param $customerGroup
181
+ * @param Mage_Core_Model_Store|number|null $store
182
+ * @return boolean
 
183
  */
184
+ public function isButtonEnabledForCustomerGroup($customerGroup, $store = null)
185
  {
186
+ return $this->isFeatureEnabledForCustomerGroup(
187
+ $customerGroup,
188
+ $store,
189
+ self::XML_PATH_BUTTON_DISABLED_CUSTOMER_GROUPS
190
+ );
191
+ }
192
+ /**
193
+ * @param Mage_Core_Model_Store|number|null $store
194
+ * @return boolean Whether the PW button is enabled for the given store.
195
+ */
196
+ public function isButtonEnabledForStore($store = null)
197
+ {
198
+ $enabled = !!Mage::getStoreConfig(self::XML_PATH_BUTTON_ENABLED, $store);
199
+ $apiKey = Mage::getStoreConfig(self::XML_PATH_API_KEY, $store);
200
 
201
+ return $enabled && $apiKey;
202
+ }
203
 
204
+ /**
205
+ * @param $customerGroup
206
+ * @param Mage_Core_Model_Store|number|null $store
207
+ * @return boolean
208
+ */
209
+ public function isConversionToolsEnabledForCustomerGroup($customerGroup, $store = null)
210
+ {
211
+ return $this->isFeatureEnabledForCustomerGroup(
212
+ $customerGroup,
213
+ $store,
214
+ self::XML_PATH_CONVERSION_TOOLS_DISABLED_CUSTOMER_GROUPS
215
+ );
216
+ }
217
 
218
+ /**
219
+ * @param array $categories An array of categories (or category ids).
220
+ * @param Mage_Core_Model_Store|number|null $store
221
+ * @return boolean Whether the conversion tools are enabled for *at least one* of the given categories.
222
+ */
223
+ public function isConversionToolsEnabledForAnyCategory(array $categories, $store = null)
224
+ {
225
+ return $this->isFeatureEnabledForAnyCategory(
226
+ $categories,
227
+ $store,
228
+ 'isConversionToolsEnabled' // e.g. $nypcategory->isConversionToolsEnabled()
229
+ );
230
  }
231
 
232
  /**
233
+ * @param Mage_Core_Model_Store|number|null $store
234
+ * @return boolean Whether the PW conversion tools are enabled for the given store.
 
 
 
235
  */
236
+ public function isConversionToolsEnabledForStore($store = null)
237
  {
238
+ $enabled = !!Mage::getStoreConfig(self::XML_PATH_CONVERSION_TOOLS_ENABLED, $store);
239
+ $apiKey = Mage::getStoreConfig(self::XML_PATH_API_KEY, $store);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
 
241
+ return $enabled && $apiKey;
242
+ }
243
 
244
+ /**
245
+ * @param array $categories
246
+ * @param Mage_Core_Model_Store|number|null $store
247
+ * @param string $categoryGetter
248
+ * @return boolean
249
+ */
250
+ protected function isFeatureEnabledForAnyCategory(
251
+ array $categories,
252
+ $store,
253
+ $categoryGetter
254
+ )
255
+ {
256
+ $store = Mage::app()->getStore($store);
257
 
258
+ // See if we're even doing "disable by category"
259
+ // This helps avoid recursive category lookups...
260
+ $areDisablingByCategory = Mage::getStoreConfig(self::XML_PATH_DISABLED_BY_CATEGORY, $store);
261
+ if (!$areDisablingByCategory) {
262
+ return true;
263
  }
264
 
265
+ // Resolve the $categories array into a an actual
266
+ // array of Mage_Catalog_Model_Category instances
267
+ $resolvedCategories = array();
268
+ foreach ($categories as $cat) {
269
+ if (is_object($cat)) {
270
+ $resolvedCategories[] = $cat;
271
+ } else if (is_numeric($cat)) {
272
+ // Load category by id
273
+ $cat = Mage::getModel('catalog/category')->load($cat);
274
+ if ($cat->getId()) {
275
+ $resolvedCategories[] = $cat;
276
+ }
277
+ }
278
  }
279
 
280
+ $enabled = false;
 
281
 
282
+ foreach($resolvedCategories as $category) {
283
+ // We store category config in a parallel model.
284
+ $nypcategory = Mage::getModel('nypwidget/category')->loadByCategory($category, $store->getId());
285
+ if ($nypcategory->$categoryGetter()) {
286
+ return true;
287
+ }
 
288
  }
289
 
290
+ return false;
291
  }
292
 
293
+ /**
294
+ * @param object|number $customerGroup
295
+ * @param Mage_Core_Model_Store|number|null $store
296
+ * @param string $xmlPath Path to the setting that holds the customer group ids.
297
+ * @return boolean
298
+ */
299
+ protected function isFeatureEnabledForCustomerGroup(
300
+ $customerGroup,
301
+ $store,
302
+ $xmlPath
303
+ )
304
  {
305
+ $anyDisabledByCustomerGroup = Mage::getStoreConfig(self::XML_PATH_DISABLE_BY_CUSTOMER_GROUP, $store);
 
 
 
 
 
 
 
306
 
307
+ if (!$anyDisabledByCustomerGroup) {
308
+ // Not using "disable by customer group" feature
309
+ return true;
 
 
 
 
310
  }
311
 
312
+ $id = is_object($customerGroup) ?
313
+ $customerGroup->getId() :
314
+ $customerGroup;
315
 
316
+ $customerGroupIds = Mage::getStoreConfig($xmlPath, $store);
317
+ $customerGroupIds = explode(',', $customerGroupIds);
 
 
 
 
 
 
 
 
 
318
 
319
+ // $customerGroupIds contains ids of groups for which feature is *disabled*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
 
321
+ if (in_array($id, $customerGroupIds)) {
322
+ return false;
323
  }
324
 
325
+ return true;
326
  }
327
  }
app/code/community/PriceWaiter/NYPWidget/Helper/Product.php DELETED
@@ -1,108 +0,0 @@
1
- <?php
2
-
3
- class PriceWaiter_NYPWidget_Helper_Product extends Mage_Core_Helper_Abstract
4
- {
5
- public function lookupData(Array $productConfiguration) {
6
- $productInformation = array();
7
- $productInformation['allow_pricewaiter'] = Mage::helper('nypwidget')->isEnabledForStore();
8
-
9
- $cart = Mage::getModel('checkout/cart');
10
-
11
- $product = Mage::getModel('catalog/product')
12
- ->setStoreId(Mage::app()->getStore()->getId())
13
- ->load($productConfiguration['product']);
14
-
15
- // adding out-of-stock items to cart will fail
16
- try {
17
- $cart->addProduct($product, $productConfiguration);
18
- $cart->save();
19
- } catch (Mage_Core_Exception $e) {
20
- $productInformation['inventory'] = 0;
21
- $productInformation['can_backorder'] = false;
22
- return $productInformation;
23
- }
24
-
25
- $cartItem = $cart->getQuote()->getAllItems();
26
- if ($product->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_SIMPLE
27
- || $product->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_BUNDLE
28
- || $product->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_GROUPED
29
- ) {
30
- $cartItem = $cartItem[0];
31
- } else {
32
- $cartItem = $cartItem[1];
33
- }
34
-
35
- if ($product->getTypeId() != Mage_Catalog_Model_Product_Type::TYPE_GROUPED) {
36
- $product = Mage::getModel('catalog/product')->load($cartItem->getProduct()->getId());
37
- }
38
-
39
- $productFound = is_object($product) && $product->getId();
40
- if (!$productFound) {
41
- return false;
42
- }
43
-
44
- // Pull the product information from the cart item.
45
- $productType = $product->getTypeId();
46
- if ($productType == Mage_Catalog_Model_Product_Type::TYPE_SIMPLE
47
- || $productType == Mage_Catalog_Model_Product_Type::TYPE_CONFIGURABLE
48
- ) {
49
- $qty = $product->getStockItem()->getQty();
50
- $productFinalPrice = $product->getFinalPrice();
51
- $productPrice = $product->getPrice();
52
- $msrp = $product->getData('msrp');
53
- $cost = $product->getData('cost');
54
- } elseif ($productType == Mage_Catalog_Model_Product_Type::TYPE_GROUPED) {
55
- $qty = Mage::helper('nypwidget')->getGroupedQuantity($productConfiguration);
56
- $productFinalPrice = Mage::helper('nypwidget')->getGroupedFinalPrice($productConfiguration);
57
- $productPrice = $productFinalPrice;
58
- $msrp = false;
59
- $cost = Mage::helper('nypwidget')->getGroupedCost($productConfiguration);
60
- } else {
61
- $qty = $cartItem->getProduct()->getStockItem()->getQty();
62
- $productFinalPrice = $cartItem->getPrice();
63
- $productPrice = $cartItem->getFinalPrice();
64
- $msrp = $cartItem->getData('msrp');
65
- $cost = $cartItem->getData('cost');
66
- }
67
-
68
- // Check for backorders set for the site
69
- $backorder = false;
70
- if ($product->getStockItem()->getUseConfigBackorders() &&
71
- Mage::getStoreConfig('cataloginventory/item_options/backorders')
72
- ) {
73
- $backorder = true;
74
- } else if ($product->getStockItem()->getBackorders()) {
75
- $backorder = true;
76
- }
77
-
78
- // If the product is returning a '0' quantity, but is "In Stock", set the "backorder" flag to true.
79
- if ($product->getStockItem()->getIsInStock() == 1 && $qty == 0) {
80
- $backorder = true;
81
- }
82
-
83
- $productInformation['inventory'] = (int)$qty;
84
- $productInformation['can_backorder'] = $backorder;
85
-
86
- $currency = Mage::app()->getStore()->getCurrentCurrencyCode();
87
-
88
- if ($productFinalPrice != 0) {
89
- $productInformation['retail_price'] = (string)$productFinalPrice;
90
- $productInformation['retail_price_currency'] = $currency;
91
- }
92
-
93
- if ($msrp != '') {
94
- $productInformation['regular_price'] = (string)$msrp;
95
- $productInformation['regular_price_currency'] = $currency;
96
- } elseif ($productPrice != 0) {
97
- $productInformation['regular_price'] = (string)$productPrice;
98
- $productInformation['regular_price_currency'] = $currency;
99
- }
100
-
101
- if ($cost) {
102
- $productInformation['cost'] = (string)$cost;
103
- $productInformation['cost_currency'] = (string)$productInformation['retail_price_currency'];
104
- }
105
-
106
- return $productInformation;
107
- }
108
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/code/community/PriceWaiter/NYPWidget/Model/Callback.php CHANGED
@@ -393,7 +393,7 @@ class PriceWaiter_NYPWidget_Model_Callback
393
  $productSku = $request['product_sku'];
394
  $productOptions = $this->buildProductOptionsArray($request);
395
 
396
- $product = $this->getHelper()->getProductWithOptions($productSku, $productOptions);
397
 
398
  return $product->getId() ? $product : false;
399
  }
@@ -433,6 +433,47 @@ class PriceWaiter_NYPWidget_Model_Callback
433
  );
434
  }
435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  /**
437
  * @param Array $request
438
  * @return Mage_Core_Model_Store
@@ -661,6 +702,55 @@ class PriceWaiter_NYPWidget_Model_Callback
661
  return $valid;
662
  }
663
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
664
  /**
665
  * @internal
666
  * @param Mage_Sales_Model_Order $order
@@ -767,6 +857,66 @@ class PriceWaiter_NYPWidget_Model_Callback
767
  ->save();
768
  }
769
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  /**
771
  * @param Mage_Sales_Model_Order $order
772
  */
393
  $productSku = $request['product_sku'];
394
  $productOptions = $this->buildProductOptionsArray($request);
395
 
396
+ $product = $this->getProductWithOptions($productSku, $productOptions);
397
 
398
  return $product->getId() ? $product : false;
399
  }
433
  );
434
  }
435
 
436
+ /**
437
+ * Finds the Product that matches the given options and SKU
438
+ * @param {String} $sku SKU of the product
439
+ * @param {Array} $productOptions An array of options for the product, name => value
440
+ * @return {Object} Returns Mage_Catalog_Model_Product of product that matches options.
441
+ * @throws PriceWaiter_NYPWidget_Exception_Product_NotFound If no product can be found.
442
+ */
443
+ public function getProductWithOptions($sku, $productOptions)
444
+ {
445
+ $product = Mage::getModel('catalog/product')->getCollection()
446
+ ->addAttributeToFilter('sku', $sku)
447
+ ->addAttributeToSelect('*')
448
+ ->getFirstItem();
449
+
450
+ if (!$product->getId()) {
451
+ throw new PriceWaiter_NYPWidget_Exception_Product_NotFound();
452
+ }
453
+
454
+ // NOTE: If buyer was looking at a configurable product,
455
+ // SKU *should* be set to that of the simple product generated
456
+ // based on their configuration. It $product is configurable,
457
+ // it indicates that either:
458
+ //
459
+ // 1. SKU of child product was not properly resolved client-side
460
+ // before offer was submitted.
461
+ //
462
+ // - or -
463
+ //
464
+ // 2. Something is messed up with SKUs in this store.
465
+ if ($product->getTypeId() == 'configurable') {
466
+ $product = $this->resolveConfigurableProductForOrderWrite(
467
+ $product,
468
+ $productOptions
469
+ );
470
+ }
471
+
472
+ $this->applyCustomOptionPricesToProduct($product, $productOptions);
473
+
474
+ return $product;
475
+ }
476
+
477
  /**
478
  * @param Array $request
479
  * @return Mage_Core_Model_Store
702
  return $valid;
703
  }
704
 
705
+ /**
706
+ * For any custom options in use, modify the product's price accordingly.
707
+ * @param Mage_Catalog_Model_Product $product
708
+ * @param array $productOptions
709
+ */
710
+ protected function applyCustomOptionPricesToProduct(
711
+ Mage_Catalog_Model_Product $product,
712
+ array $productOptions
713
+ )
714
+ {
715
+ // Check if any values in $productOptions map to custom options
716
+ // available on the product.
717
+ $options = $product->getOptions();
718
+
719
+ $amountToAdd = 0;
720
+
721
+ foreach($options as $opt) {
722
+ if (!array_key_exists($opt->getTitle(), $productOptions)) {
723
+ continue;
724
+ }
725
+
726
+ $productOptionValue = $productOptions[$opt->getTitle()];
727
+
728
+ // If the option changes the *price* of the product, attempt to
729
+ // get that change reflected. If this fails, order *total* will
730
+ // still be accurate, but the applied PriceWaiter discount will be
731
+ // wonky.
732
+ // This is not 100% accurate--it's possible that option names could
733
+ // change or be tweaked on the client side. But it's good enough
734
+ //
735
+
736
+ // 1. Apply any price that this *option* alone has
737
+ $amountToAdd += $opt->getPrice(true);
738
+
739
+ // 2. If the option has values, see if any of them are selected
740
+ // and apply their price changes.
741
+ foreach($opt->getValues() as $v) {
742
+ if ($v->getTitle() === $productOptionValue) {
743
+ $amountToAdd += $v->getPrice(true);
744
+ break;
745
+ }
746
+ };
747
+ }
748
+
749
+ // Apply custom option price changes all in one go
750
+ // (to avoid them interfering with each other)
751
+ $product->setPrice($product->getPrice() + $amountToAdd);
752
+ }
753
+
754
  /**
755
  * @internal
756
  * @param Mage_Sales_Model_Order $order
857
  ->save();
858
  }
859
 
860
+ /**
861
+ * Attempts to look up a simple product based on a configurable product + a
862
+ * hash of PriceWaiter product options.
863
+ */
864
+ protected function resolveConfigurableProductForOrderWrite(
865
+ Mage_Catalog_Model_Product $product,
866
+ array &$productOptions
867
+ )
868
+ {
869
+ if ($product->getTypeId() !== 'configurable') {
870
+ return $product;
871
+ }
872
+
873
+ $attrs = $product->getTypeInstance(true)->getConfigurableAttributesAsArray($product);
874
+ $attributesForLookup = array();
875
+ $additionalCost = 0;
876
+
877
+ // Resolve product options into attribute id/value pairs
878
+ foreach ($attrs as $attr) {
879
+ if (!array_key_exists($attr['label'], $productOptions)) {
880
+ // No product option value exists for this attribute.
881
+ // This will most likely make the lookup fail.
882
+ continue;
883
+ }
884
+
885
+ $productOptionValue = $productOptions[$attr['label']];
886
+ $valueIndex = null;
887
+
888
+ // Find the corresponding attribute value
889
+ foreach ($attr['values'] as $value) {
890
+ if ($value['label'] === $productOptionValue) {
891
+ $valueIndex = $value['value_index'];
892
+ // If this attribute has a price assosciated with it, add it to the price later
893
+ if ($value['pricing_value']) {
894
+ $additionalCost += $value['pricing_value'];
895
+ }
896
+ break;
897
+ }
898
+ }
899
+
900
+ if ($valueIndex !== null) {
901
+ // We found a corresponding attribute to look for
902
+ $attributesForLookup[$attr['attribute_id']] = $valueIndex;
903
+ }
904
+ }
905
+
906
+ $simpleProduct = $product
907
+ ->getTypeInstance()
908
+ ->getProductByAttributes($attributesForLookup, $product);
909
+
910
+ if (!$product || !$product->getId()) {
911
+ throw new PriceWaiter_NYPWidget_Exception_Product_NotFound();
912
+ }
913
+
914
+ $product->load($product->getId());
915
+ $product->setPrice($product->getPrice() + $additionalCost);
916
+
917
+ return $product;
918
+ }
919
+
920
  /**
921
  * @param Mage_Sales_Model_Order $order
922
  */
app/code/community/PriceWaiter/NYPWidget/Model/Callback/Inventory.php CHANGED
@@ -48,13 +48,8 @@ class PriceWaiter_NYPWidget_Model_Callback_Inventory
48
  }
49
  catch (Mage_Core_Exception $ex)
50
  {
51
- // There's not really a great way to be *sure* this is the "not enough inventory" error
52
- // without examining the (localizable) error message.
53
- throw new PriceWaiter_NYPWidget_Exception_OutOfStock(
54
- $ex->getMessage(),
55
- 0,
56
- $ex
57
- );
58
  }
59
 
60
  // 2. If that resulted in items going out of stock, they need to be saved + reindexed.
48
  }
49
  catch (Mage_Core_Exception $ex)
50
  {
51
+ $translatedEx = PriceWaiter_NYPWidget_Exception_Abstract::translateMagentoException($ex);
52
+ throw $translatedEx;
 
 
 
 
 
53
  }
54
 
55
  // 2. If that resulted in items going out of stock, they need to be saved + reindexed.
app/code/community/PriceWaiter/NYPWidget/Model/Deal.php ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * A Deal is a single opportunity for a single buyer to purchase one or more
5
+ * products at a certain price.
6
+ */
7
+ class PriceWaiter_NYPWidget_Model_Deal extends Mage_Core_Model_Abstract
8
+ {
9
+ /**
10
+ * Querystring arg used to specify deal id.
11
+ */
12
+ const CHECKOUT_URL_DEAL_ID_ARG = 'd';
13
+
14
+ /**
15
+ * @var Array
16
+ */
17
+ private $_offerItems = null;
18
+
19
+ /**
20
+ * @var Array
21
+ */
22
+ private $_resolvedItems = null;
23
+
24
+ /**
25
+ * {@inheritdoc}
26
+ */
27
+ public function __construct()
28
+ {
29
+ $this->_init('nypwidget/deal');
30
+ parent::__construct();
31
+ }
32
+
33
+ /**
34
+ * Initializes the parameters of this deal from a request made to the
35
+ * "create deal" endpoint.
36
+ * Does not actually save this Deal to the Db.
37
+ * @return PriceWaiter_NYPWidget_Model_Deal $this
38
+ */
39
+ public function initFromCreateRequest(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request)
40
+ {
41
+ $body = $request->getBody();
42
+
43
+ if (!empty($body->test)) {
44
+ throw new PriceWaiter_NYPWidget_Exception_NoTestDeals();
45
+ }
46
+
47
+ $this->setId($body->id);
48
+ $this->setCreateRequestId($request->getId());
49
+ $this->setCreatedAt(date('Y-m-d H:i:s', $request->getTimestamp()));
50
+
51
+ $storeId = $this->getStoreIdForApiKey($request->getApiKey());
52
+ if (!$storeId) {
53
+ throw new PriceWaiter_NYPWidget_Exception_ApiKey();
54
+ }
55
+ $this->setStoreId($storeId);
56
+
57
+ if (!empty($body->expires_at)) {
58
+ $expires = strtotime($body->expires_at);
59
+ $this->setExpiresAt(date('Y-m-d H:i:s', $expires));
60
+ }
61
+
62
+ $this->setPricewaiterBuyerId($body->buyer->id);
63
+
64
+ // Stash full JSON for create request, so we have access to items later on.
65
+ $this->setCreateRequestBodyJson($body);
66
+
67
+ return $this;
68
+ }
69
+
70
+ /**
71
+ */
72
+ public function ensurePresentInQuote(Mage_Sales_Model_Quote $quote)
73
+ {
74
+ $items = $this->getOfferItems();
75
+
76
+ foreach($items as $item) {
77
+ $item->ensurePresentInQuote($quote);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * @return Array Array of PriceWaiter_NYPWidget_Model_OfferItem instances.
83
+ */
84
+ public function getOfferItems()
85
+ {
86
+ if (!is_null($this->_offerItems)) {
87
+ return $this->_offerItems;
88
+ }
89
+
90
+ $this->_offerItems = array_map(
91
+ function($itemData) {
92
+ return Mage::getModel('nypwidget/offer_item', $itemData);
93
+ },
94
+ $this->getCreateRequestBodyField('items', array())
95
+ );
96
+
97
+ return $this->_offerItems;
98
+ }
99
+
100
+ /**
101
+ * @param String $formKey Magento form key used for the add to cart form.
102
+ * @return String A URL that, when followed, will end up on the cart page with this deal applied.
103
+ * @throws PriceWaiter_NYPWidget_Exception_SingleItemOnly
104
+ */
105
+ public function getAddToCartUrl($formKey)
106
+ {
107
+ $offerItems = $this->getOfferItems();
108
+
109
+ if (count($offerItems) !== 1) {
110
+ throw new PriceWaiter_NYPWidget_Exception_SingleItemOnly();
111
+ }
112
+
113
+ $onlyItem = $offerItems[0];
114
+ $formValues = $onlyItem->getAddToCartForm();
115
+
116
+ $addToCartQuery = array(
117
+ 'form_key' => $formKey,
118
+ 'product' => $formValues['product'],
119
+ 'qty' => $onlyItem->getMinimumQuantity(),
120
+ );
121
+
122
+ if (isset($formValues['super_attribute'])) {
123
+ $addToCartQuery['super_attribute'] = $formValues['super_attribute'];
124
+ }
125
+
126
+ $urls = array(
127
+ // 1. Add to cart.
128
+ array(
129
+ 'checkout/cart/add',
130
+ '_query' => $addToCartQuery,
131
+ ),
132
+ );
133
+
134
+ return array_reduce(array_reverse($urls), function($prevUrl, $params) {
135
+
136
+ $path = array_shift($params);
137
+
138
+ if ($prevUrl) {
139
+ $params['_query']['return_url'] = $prevUrl;
140
+ }
141
+
142
+ return Mage::getUrl($path, $params);
143
+
144
+ });
145
+ }
146
+
147
+ /**
148
+ * @return String A publically routeable URL to take advantage of this deal.
149
+ */
150
+ public function getCheckoutUrl()
151
+ {
152
+ return Mage::getUrl("_dealpw/checkout", array(
153
+ '_store' => $this->getStoreId(),
154
+ '_query' => array(
155
+ self::CHECKOUT_URL_DEAL_ID_ARG => $this->getId(),
156
+ ),
157
+ ));
158
+ }
159
+
160
+ /**
161
+ * @return Mage_Core_Model_Store The Store this deal is part of.
162
+ */
163
+ public function getStore()
164
+ {
165
+ $id = $this->getStoreId();
166
+ if ($id) {
167
+ $store = Mage::getModel('core/store');
168
+ return $store->load($id);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * @param Integer $now UNIX timestamp representing current time.
174
+ * @return boolean
175
+ */
176
+ public function isExpired($now = null)
177
+ {
178
+ $expiry = $this->getExpiresAt();
179
+
180
+ if (empty($expiry)) {
181
+ return false;
182
+ }
183
+
184
+ $expiry = strtotime($expiry);
185
+ $now = ($now === null ? time() : $now);
186
+
187
+ return $expiry < $now;
188
+ }
189
+
190
+ /**
191
+ * Nice alias for getRevoked()
192
+ * @return boolean
193
+ */
194
+ public function isRevoked()
195
+ {
196
+ return !!$this->getRevoked();
197
+ }
198
+
199
+ /**
200
+ * @return PriceWaiter_NYPWidget_Model_Deal $this
201
+ */
202
+ public function processCreateRequest(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request)
203
+ {
204
+ $this->initFromCreateRequest($request);
205
+
206
+ $existingDeal = Mage::getModel('nypwidget/deal')->load($this->getId());
207
+
208
+ if ($existingDeal && $existingDeal->getId()) {
209
+ // This deal has already been created.
210
+ throw new PriceWaiter_NYPWidget_Exception_DealAlreadyCreated();
211
+ }
212
+
213
+ $saveTransaction = Mage::getModel('core/resource_transaction');
214
+ $saveTransaction->addObject($this);
215
+ $saveTransaction->save();
216
+
217
+ return $this;
218
+ }
219
+
220
+ /**
221
+ * Main "Revoke Deal" logic.
222
+ * @param PriceWaiter_NYPWidget_Controller_Endpoint_Request $request
223
+ * @return PriceWaiter_NYPWidget_Model_Deal $this
224
+ */
225
+ public function processRevokeRequest(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request)
226
+ {
227
+ if ($this->revoked) {
228
+ throw new PriceWaiter_NYPWidget_Exception_DealAlreadyRevoked();
229
+ }
230
+
231
+ $this->setRevoked(1);
232
+ $this->setRevokeRequestId($request->getId());
233
+ $this->setRevokedAt(date('Y-m-d H:i:s', $request->getTimestamp()));
234
+
235
+ $saveTransaction = Mage::getModel('core/resource_transaction');
236
+ $saveTransaction->addObject($this);
237
+ $saveTransaction->save();
238
+
239
+ return $this;
240
+ }
241
+
242
+ /**
243
+ * @internal
244
+ * @param String $json
245
+ */
246
+ public function setCreateRequestBodyJson($json)
247
+ {
248
+ if (!is_string($json)) {
249
+ $json = json_encode($json);
250
+ }
251
+
252
+ $this->setData('create_request_body_json', $json);
253
+ $this->_createRequestBody = null;
254
+ $this->_items = null;
255
+
256
+ return $this;
257
+ }
258
+
259
+ /**
260
+ * @internal Reads a field off the original "create" request.
261
+ * @param String $key
262
+ * @param Mixed $default
263
+ * @return Mixed
264
+ */
265
+ protected function getCreateRequestBodyField($key, $default = null)
266
+ {
267
+ if ($this->_createRequestBody === null) {
268
+ // Parse it!
269
+ $this->_createRequestBody = json_decode($this->getCreateRequestBodyJson());
270
+ }
271
+
272
+ $b = $this->_createRequestBody;
273
+ return isset($b->$key) ? $b->$key : null;
274
+ }
275
+
276
+ protected function getStoreIdForApiKey($apiKey)
277
+ {
278
+ $helper = Mage::helper('nypwidget');
279
+ $store = $helper->getStoreByPriceWaiterApiKey($apiKey);
280
+ return $store ? $store->getId() : false;
281
+ }
282
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Discounter.php ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class responsible for calculating PriceWaiter discounts.
5
+ */
6
+ class PriceWaiter_NYPWidget_Model_Discounter
7
+ {
8
+ protected $offerAmountPerItem = null;
9
+ protected $offerMinQty = 1;
10
+ protected $offerMaxQty = 1;
11
+
12
+ /**
13
+ * @var Mage_Directory_Model_Currency
14
+ */
15
+ protected $offerCurrency = null;
16
+
17
+ /**
18
+ * @var double
19
+ */
20
+ protected $productPrice = 0;
21
+
22
+ /**
23
+ * @var Mage_Directory_Model_Currency
24
+ */
25
+ protected $quoteBaseCurrency = null;
26
+
27
+ /**
28
+ * @var Mage_Directory_Model_Currency
29
+ */
30
+ protected $quoteCurrency = null;
31
+
32
+ /**
33
+ * @var integer
34
+ */
35
+ protected $quoteItemQty = 1;
36
+
37
+
38
+ /**
39
+ * @param Mage_Directory_Model_Currency $currency
40
+ */
41
+ public function setQuoteBaseCurrency(Mage_Directory_Model_Currency $currency)
42
+ {
43
+ $this->quoteBaseCurrency = $currency;
44
+ return $this;
45
+ }
46
+
47
+ /**
48
+ * @param Mage_Directory_Model_Currency $currency
49
+ */
50
+ public function setQuoteCurrency(Mage_Directory_Model_Currency $currency)
51
+ {
52
+ $this->quoteCurrency = $currency;
53
+ return $this;
54
+ }
55
+
56
+ /**
57
+ * @param double|string $amount
58
+ */
59
+ public function setOfferAmountPerItem($amount)
60
+ {
61
+ $this->offerAmountPerItem = $amount;
62
+ return $this;
63
+ }
64
+
65
+ /**
66
+ * @param Mage_Directory_Model_Currency $currency
67
+ */
68
+ public function setOfferCurrency(Mage_Directory_Model_Currency $currency)
69
+ {
70
+ $this->offerCurrency = $currency;
71
+ return $this;
72
+ }
73
+
74
+ /**
75
+ * @param integer $qty
76
+ */
77
+ public function setOfferMinQty($qty)
78
+ {
79
+ $this->offerMinQty = $qty;
80
+ return $this;
81
+ }
82
+
83
+ /**
84
+ * @param integer $qty
85
+ */
86
+ public function setOfferMaxQty($qty)
87
+ {
88
+ $this->offerMaxQty = $qty;
89
+ return $this;
90
+ }
91
+
92
+ /**
93
+ * Product's price, expressed in the quote's currency.
94
+ * @param double|string $price
95
+ */
96
+ public function setProductPrice($price)
97
+ {
98
+ $this->productPrice = $price;
99
+ return $this;
100
+ }
101
+
102
+ /**
103
+ * Product's "original price", expressed in the quote's currency.
104
+ * @param double|string $price
105
+ */
106
+ public function setProductOriginalPrice($price)
107
+ {
108
+ $this->productOriginalPrice = $price;
109
+ return $this;
110
+ }
111
+
112
+ /**
113
+ * @param Integer $qty Quantity being ordered.
114
+ */
115
+ public function setQuoteItemQty($qty)
116
+ {
117
+ $this->quoteItemQty = $qty;
118
+ return $this;
119
+ }
120
+
121
+ /**
122
+ * @return double The discount amount to apply to the quote item (as a positive number), expressed in the quote's currency.
123
+ */
124
+ public function getDiscount()
125
+ {
126
+ return $this->calculateDiscount(
127
+ $this->productPrice,
128
+ $this->quoteCurrency
129
+ );
130
+ }
131
+
132
+ /**
133
+ * @return double The discount amount to apply to the quote item, expressed in the quote's base currency.
134
+ */
135
+ public function getBaseDiscount()
136
+ {
137
+ return $this->calculateDiscount(
138
+ $this->productPrice,
139
+ $this->quoteBaseCurrency
140
+ );
141
+ }
142
+
143
+ /**
144
+ * @return double The discount amount over the original price (in quote currency)
145
+ */
146
+ public function getOriginalDiscount()
147
+ {
148
+ $price = $this->productOriginalPrice ?
149
+ $this->productOriginalPrice :
150
+ $this->productPrice;
151
+
152
+ return $this->calculateDiscount($price, $this->quoteCurrency);
153
+ }
154
+
155
+ /**
156
+ * @return double The discount amount over the product's base price (in quote base currency)
157
+ */
158
+ public function getBaseOriginalDiscount()
159
+ {
160
+ $price = $this->productOriginalPrice ?
161
+ $this->productOriginalPrice :
162
+ $this->productPrice;
163
+
164
+ return $this->calculateDiscount($price, $this->quoteBaseCurrency);
165
+ }
166
+
167
+ /**
168
+ * Calculates a discount in the given currency.
169
+ * @param double|string $price Product price
170
+ */
171
+ protected function calculateDiscount($priceInQuoteCurrency, Mage_Directory_Model_Currency $currency)
172
+ {
173
+ $effectiveQty = min($this->quoteItemQty, $this->offerMaxQty);
174
+
175
+ if ($effectiveQty < $this->offerMinQty) {
176
+ // Not enough of item on quote to qualify.
177
+ return 0;
178
+ }
179
+
180
+ // Standardize offer into quote currency
181
+ $amountPerItemInQuoteCurrency = $this->offerCurrency->convert(
182
+ $this->offerAmountPerItem,
183
+ $this->quoteCurrency
184
+ );
185
+
186
+ if ($amountPerItemInQuoteCurrency >= $priceInQuoteCurrency) {
187
+ // Offer is for more than product price. No discount applied.
188
+ return 0;
189
+ }
190
+
191
+ $discountInQuoteCurrency = ($priceInQuoteCurrency - $amountPerItemInQuoteCurrency) * $effectiveQty;
192
+ return $this->quoteCurrency->convert($discountInQuoteCurrency, $currency);
193
+ }
194
+
195
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Embed.php ADDED
@@ -0,0 +1,520 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Model responsible for figuring how to format PW's embed on the product page.
5
+ *
6
+ * @method Mage_Core_Model_Store getStore() Gets the store being embedded on.
7
+ * @method Mage_Customer_Model_Customer getCustomer()
8
+ * @method Mage_Catalog_Model_Product getProduct()
9
+ * @method Mage_Catalog_Model_Category getCategory()
10
+ */
11
+ class PriceWaiter_NYPWidget_Model_Embed
12
+ extends Varien_Object
13
+ {
14
+ // Cache values for these because they can be expensive to compute.
15
+ protected $_isButtonEnabled = null;
16
+ protected $_isConversionToolsEnabled = null;
17
+ protected $_scriptTags = null;
18
+
19
+ /**
20
+ * Build a variable describing *all* categories product is part of.
21
+ * @param Mage_Catalog_Model_Product $product
22
+ * @param Mage_Core_Model_Store $store
23
+ * @return Array
24
+ */
25
+ public function buildCategoriesVar(Mage_Catalog_Model_Product $product, Mage_Core_Model_Store $store)
26
+ {
27
+ $categorization = array();
28
+ $assignedCategories = $product->getCategoryCollection()
29
+ ->addAttributeToSelect('name');
30
+
31
+ $baseUrl = $store->getBaseUrl();
32
+
33
+ // Find the path (parents) of each category, and add their information
34
+ // to the categorization array
35
+ foreach ($assignedCategories as $assignedCategory) {
36
+ $parentCategories = array();
37
+ $path = $assignedCategory->getPath();
38
+ $parentIds = explode('/', $path);
39
+ array_shift($parentIds); // We don't care about the root category
40
+
41
+ $categoryModel = Mage::getModel('catalog/category');
42
+ foreach($parentIds as $parentCategoryId) {
43
+ $parentCategory = $categoryModel->load($parentCategoryId);
44
+ $parentCategoryUrl = preg_replace('/^\//', '', $parentCategory->getUrlPath());
45
+
46
+ $parentCategories[] = array(
47
+ 'name' => $parentCategory->getName(),
48
+ 'url' => $baseUrl . '/' . $parentCategoryUrl
49
+ );
50
+ }
51
+
52
+ $categorization[] = $parentCategories;
53
+ }
54
+
55
+ return $categorization;
56
+ }
57
+
58
+ /**
59
+ * Builds a variable describing the custom options configuration for $product.
60
+ * @param Mage_Catalog_Model_Product $product
61
+ * @return Array
62
+ */
63
+ public function buildCustomOptionsVar(Mage_Catalog_Model_Product $product)
64
+ {
65
+ $options = $product->getOptions();
66
+ $result = array();
67
+
68
+ foreach($options as $opt) {
69
+ $jsonOpt = array(
70
+ 'id' => $opt->getId(),
71
+ 'type' => $opt->getType(),
72
+ 'title' => $opt->getTitle(),
73
+ 'required' => !!$opt->getIsRequire(),
74
+ );
75
+
76
+ $sku = $opt->getSku();
77
+ if ($sku !== null && $sku !== '') {
78
+ $jsonOpt['sku'] = $opt->getSku();
79
+ }
80
+
81
+ $values = $opt->getValues();
82
+ if ($values) {
83
+ $jsonOpt['values'] = array();
84
+ foreach($values as $v) {
85
+ $jsonValue = array(
86
+ 'id' => $v->getId(),
87
+ 'title' => $v->getTitle(),
88
+ );
89
+
90
+ $sku = $v->getSku();
91
+ if ($sku !== null && $sku !== '') {
92
+ $jsonValue['sku'] = $v->getSku();
93
+ }
94
+
95
+ $jsonOpt['values'][] = $jsonValue;
96
+ }
97
+ }
98
+
99
+ $result[] = $jsonOpt;
100
+ }
101
+
102
+ return $result;
103
+ }
104
+
105
+ /**
106
+ * @param Mage_Catalog_Model_Product $product
107
+ * @return Array|false A variable mapping simple product ids to skus, or false if not available.
108
+ */
109
+ public function buildIdToSkuVar(Mage_Catalog_Model_Product $product)
110
+ {
111
+ if ($product->getTypeId() !== Mage_Catalog_Model_Product_Type::TYPE_CONFIGURABLE) {
112
+ // This is only used for configurables
113
+ return false;
114
+ }
115
+
116
+ $simples = Mage::getModel('catalog/product_type_configurable')
117
+ ->setProduct($product)
118
+ ->getUsedProductCollection()
119
+ ->addAttributeToSelect('sku');
120
+
121
+ $idsToSkus = array();
122
+
123
+ foreach ($simples as $simple) {
124
+ $id = $simple->getId();
125
+ $sku = $simple->getSku();
126
+ $idsToSkus[$id] = $sku;
127
+ }
128
+
129
+ return count($idsToSkus) > 0 ? $idsToSkus : false;
130
+ }
131
+
132
+ /**
133
+ * @return Object An object containing the PriceWaiterOptions structure.
134
+ */
135
+ public function buildPriceWaiterOptionsVar()
136
+ {
137
+ $options = new StdClass();
138
+
139
+ if (!$this->isButtonEnabled()) {
140
+ $options->enableButton = false;
141
+ }
142
+
143
+ if (!$this->isConversionToolsEnabled()) {
144
+ $options->enableConversionTools = false;
145
+ }
146
+
147
+ $currency = $this->getStore()->getCurrentCurrencyCode();
148
+ if ($currency) {
149
+ $options->currency = $currency;
150
+ }
151
+
152
+ $options->metadata = (object)array(
153
+ '_magento_version' => Mage::helper('nypwidget/about')->getPlatformVersion(),
154
+ '_magento_extension_version' => Mage::helper('nypwidget/about')->getExtensionVersion(),
155
+ );
156
+
157
+ $product = $this->getProduct();
158
+ if ($product) {
159
+ $options->product = $this->buildProductObject($product);
160
+ }
161
+
162
+ // Set user.email and postal_code when available
163
+ $customer = $this->getCustomer();
164
+ if ($customer && $customer->getId()) {
165
+ $options->user = new StdClass();
166
+ $options->user->email = $customer->getEmail();
167
+
168
+ $addr = $customer->getDefaultShippingAddress();
169
+ if ($addr && $addr->getId()) {
170
+ $postcode = $addr->getPostcode();
171
+ if ($postcode) {
172
+ $options->postal_code = $postcode;
173
+ }
174
+
175
+ $country = $addr->getCountryId(); // 2-char ISO code, e.g. 'US'
176
+ if ($country) {
177
+ $options->country = $country;
178
+ }
179
+ }
180
+ }
181
+
182
+ return $options;
183
+ }
184
+
185
+ /**
186
+ * Builds the `product` object for PriceWaiterOptions.
187
+ * @param Mage_Catalog_Model_Product $product
188
+ * @return Object
189
+ */
190
+ public function buildProductObject(Mage_Catalog_Model_Product $product)
191
+ {
192
+ $result = new StdClass();
193
+ $result->sku = $product->getSku();
194
+ $result->name = $product->getName();
195
+
196
+ $brand = $this->getProductBrand($product);
197
+ if ($brand !== false) {
198
+ $result->brand = $brand;
199
+ }
200
+
201
+ $image = $product->getImageUrl();
202
+ if ($image) {
203
+ $result->image = $image;
204
+ }
205
+
206
+ // If possible, set the base price.
207
+ // For configurables etc, platform JS will have to take over and
208
+ // dynamically calculate price.
209
+ $price = $product->getFinalPrice();
210
+ if ($price > 0) {
211
+ $result->price = $price;
212
+ }
213
+
214
+ return $result;
215
+ }
216
+
217
+ /**
218
+ * @return array An array of script tags to be rendered onto the page.
219
+ */
220
+ public function getScriptTags()
221
+ {
222
+ // NOTE: Anything coming out of this method is rendered directly to
223
+ // the page. Be sure to escape your HTML etc.
224
+
225
+ if ($this->_scriptTags !== null) {
226
+ return $this->_scriptTags;
227
+ }
228
+
229
+ $store = $this->getStore();
230
+
231
+ $helper = Mage::helper('nypwidget');
232
+ $widgetJsUrl = $helper->getWidgetJsUrl($store);
233
+
234
+ if (!$widgetJsUrl) {
235
+ // Can't actually embed.
236
+ $this->_scriptTags = array();
237
+ return $this->_scriptTags;
238
+ }
239
+
240
+ $JSON_OPTIONS = 0;
241
+ if (defined('JSON_UNESCAPED_SLASHES')) {
242
+ $JSON_OPTIONS |= JSON_UNESCAPED_SLASHES;
243
+ }
244
+
245
+ // First, a <script> tag containing global JS variables used
246
+ // by our platform JS.
247
+
248
+ $variablesTag = array('<script>');
249
+ foreach($this->getJavascriptVariables() as $name => $value) {
250
+ $value = json_encode($value, $JSON_OPTIONS);
251
+ $variablesTag[] = "var $name = $value;";
252
+ }
253
+ $variablesTag[] = '</script>';
254
+
255
+ // Then, the actual PW embed
256
+ $embedTag = array(
257
+ '<script src="',
258
+ htmlspecialchars($widgetJsUrl),
259
+ '" async></script>',
260
+ );
261
+
262
+ $this->_scriptTags = array(
263
+ implode('', $variablesTag),
264
+ implode('', $embedTag),
265
+ );
266
+ return $this->_scriptTags;
267
+ }
268
+
269
+ /**
270
+ * @return Array An array of global variables to register on the page. Key is variable name, value is value (to be JSON-encoded);
271
+ */
272
+ public function getJavascriptVariables()
273
+ {
274
+ $vars = array(
275
+ 'PriceWaiterOptions' => $this->buildPriceWaiterOptionsVar(),
276
+ );
277
+
278
+ $product = $this->getProduct();
279
+ $store = $this->getStore();
280
+
281
+ if ($product) {
282
+ // Provide a hint to JS about what *kind* of product we're looking at.
283
+ $vars['PriceWaiterProductType'] = $product->getTypeId();
284
+
285
+ // Platform JS picks this up to refer back to.
286
+ // TODO: It seems like we shoudl not have to provide PriceWaiterRegularPrice--
287
+ // there should be enough data available already on the frontend to
288
+ // figure this out.
289
+ $vars['PriceWaiterRegularPrice'] = (double)$product->getPrice();
290
+
291
+ // Provide frontend a means to map product ids to skus
292
+ // (Used for configurable product support)
293
+ $idsToSkus = $this->buildIdToSkuVar($product);
294
+ if ($idsToSkus !== false) {
295
+ $vars['PriceWaiterIdToSkus'] = $idsToSkus;
296
+ }
297
+
298
+ // Provide detailed information about product categories
299
+ $vars['PriceWaiterCategories'] = $this->buildCategoriesVar($product, $store);
300
+
301
+ // Provide data about custom options (SKU modifiers + labels, mostly)
302
+ $custom = $this->buildCustomOptionsVar($product);
303
+ if ($custom) {
304
+ $vars['PriceWaiterCustomOptions'] = $custom;
305
+ }
306
+ }
307
+
308
+ return $vars;
309
+ }
310
+
311
+ /**
312
+ * @return boolean Whether the PW button is enabled.
313
+ */
314
+ public function isButtonEnabled()
315
+ {
316
+ if ($this->_isButtonEnabled === null) {
317
+ $this->_isButtonEnabled = $this->isFeatureEnabled(
318
+ 'isButtonEnabledForStore',
319
+ 'isButtonEnabledForCustomerGroup',
320
+ 'isButtonEnabledForAnyCategory'
321
+ );
322
+ }
323
+
324
+ return $this->_isButtonEnabled;
325
+ }
326
+
327
+ /**
328
+ * @return boolean Whether PW's non-button "conversion tools" are enabled.
329
+ */
330
+ public function isConversionToolsEnabled()
331
+ {
332
+ if ($this->_isConversionToolsEnabled === null) {
333
+ $this->_isConversionToolsEnabled = $this->isFeatureEnabled(
334
+ 'isConversionToolsEnabledForStore',
335
+ 'isConversionToolsEnabledForCustomerGroup',
336
+ 'isConversionToolsEnabledForAnyCategory'
337
+ );
338
+ }
339
+
340
+ return $this->_isConversionToolsEnabled;
341
+ }
342
+
343
+ /**
344
+ * @param Mage_Catalog_Model_Product $product
345
+ * @return String|false
346
+ */
347
+ public function getProductBrand(Mage_Catalog_Model_Product $product)
348
+ {
349
+ // prefer brand, but fallback to manufacturer attribute
350
+ $brand = $product->getData('brand');
351
+
352
+ if ($brand) {
353
+ return $brand;
354
+ }
355
+
356
+ $attributesToTry = array(
357
+ 'manufacturer',
358
+ 'c2c_brand',
359
+ );
360
+
361
+ foreach($attributesToTry as $attr) {
362
+ $value = $this->safeGetAttributeText($product, $attr);
363
+ if ($value) {
364
+ return $value;
365
+ }
366
+ }
367
+
368
+ return false;
369
+ }
370
+
371
+ /**
372
+ * @param Mage_Catalog_Model_Category $category
373
+ */
374
+ public function setCategory($category)
375
+ {
376
+ $this->invalidateCachedValues();
377
+ return parent::setCategory($category);
378
+ }
379
+
380
+ /**
381
+ * @param Number $id
382
+ */
383
+ public function setCustomerGroupId($id)
384
+ {
385
+ $this->invalidateCachedValues();
386
+ return parent::setCustomerGroupId($id);
387
+ }
388
+
389
+ /**
390
+ * @param Mage_Catalog_Model_Product $product
391
+ */
392
+ public function setProduct($product)
393
+ {
394
+ $this->invalidateCachedValues();
395
+ return parent::setProduct($product);
396
+ }
397
+
398
+ /**
399
+ * @param Mage_Core_Model_Store $store
400
+ */
401
+ public function setStore($store)
402
+ {
403
+ $this->invalidateCachedValues();
404
+ return parent::setStore($store);
405
+ }
406
+
407
+ /**
408
+ * @return Boolean Whether we should render the placeholder span.
409
+ */
410
+ public function shouldRenderButtonPlaceholder()
411
+ {
412
+ // 1. If no <script> tags to render, no placeholder
413
+ $scriptTags = $this->getScriptTags();
414
+ if (empty($scriptTags)) {
415
+ return false;
416
+ }
417
+
418
+ // If Button + Conversion tools disabled, no placeholder required.
419
+ // (Exit Intent can lead to button being shown later, so we need
420
+ // the placeholder present.)
421
+ if ($this->isButtonEnabled() || $this->isConversionToolsEnabled()) {
422
+ return true;
423
+ }
424
+
425
+ return false;
426
+ }
427
+
428
+ /**
429
+ * @internal Resets cached values
430
+ */
431
+ protected function invalidateCachedValues()
432
+ {
433
+ $this->_isButtonEnabled = null;
434
+ $this->_isConversionToolsEnabled = null;
435
+ $this->_scriptTags = null;
436
+ }
437
+
438
+ /**
439
+ * @internal
440
+ * @return boolean
441
+ */
442
+ protected function isFeatureEnabled(
443
+ $isEnabledForStore,
444
+ $isEnabledForCustomerGroup,
445
+ $isEnabledForAnyCategory
446
+ )
447
+ {
448
+ $store = $this->getStore();
449
+ $product = $this->getProduct();
450
+
451
+ if (!$store) {
452
+ return false;
453
+ }
454
+
455
+ if (!$product) {
456
+ return false;
457
+ }
458
+
459
+ $helper = Mage::helper('nypwidget');
460
+
461
+ if (!$helper->$isEnabledForStore($store)) {
462
+ // Store has us globally disabled
463
+ return false;
464
+ }
465
+
466
+ $customerGroupId = $this->getCustomerGroupId();
467
+ if (!$helper->$isEnabledForCustomerGroup($customerGroupId, $store)) {
468
+ // Disabled for customer group
469
+ return false;
470
+ }
471
+
472
+ $category = $this->getCategory();
473
+ if ($category) {
474
+ // We're looking at a product page via a specific category.
475
+ $enabled = $helper->$isEnabledForAnyCategory(
476
+ array($category),
477
+ $store
478
+ );
479
+
480
+ return $enabled;
481
+ }
482
+
483
+ // Look at *all* categories the product is in. If *any* of them
484
+ // enable the button, enable it here.
485
+ // We can hit this code path when viewing the product page *not*
486
+ // through the lens of a certain category (i.e. when linked from
487
+ // the home page or search results).
488
+ return $helper->$isEnabledForAnyCategory(
489
+ $product->getCategoryIds(),
490
+ $store
491
+ );
492
+ }
493
+
494
+ protected function safeGetAttributeText(Mage_Catalog_Model_Product $product, $code)
495
+ {
496
+ $value = $product->getData($code);
497
+
498
+ // prevent Magento from rendering "No" when nothing is selected.
499
+ if (!$value) {
500
+ return false;
501
+ }
502
+
503
+ $resource = $product->getResource();
504
+ if (!$resource) {
505
+ return false;
506
+ }
507
+
508
+ $attr = $resource->getAttribute($code);
509
+ if (!$attr) {
510
+ return false;
511
+ }
512
+
513
+ $frontend = $attr->getFrontend();
514
+ if (!$frontend) {
515
+ return false;
516
+ }
517
+
518
+ return $frontend->getValue($product);
519
+ }
520
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Mysql4/Deal.php ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class PriceWaiter_NYPWidget_Model_Mysql4_Deal extends Mage_Core_Model_Mysql4_Abstract
4
+ {
5
+ /**
6
+ * @internal
7
+ * deal_id is provided externally and used as the PK.
8
+ * @var boolean
9
+ */
10
+ protected $_isPkAutoIncrement = false;
11
+
12
+ public function _construct()
13
+ {
14
+ $this->_init('nypwidget/deal', 'deal_id');
15
+ }
16
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Mysql4/Deal/Collection.php ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class PriceWaiter_NYPWidget_Model_Mysql4_Deal_Collection
4
+ extends Mage_Core_Model_Mysql4_Collection_Abstract
5
+ {
6
+ public function _construct()
7
+ {
8
+ $this->_init('nypwidget/deal', 'id');
9
+ }
10
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Mysql4/Deal/Usage.php ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ class PriceWaiter_NYPWidget_Model_Mysql4_Deal_Usage extends Mage_Core_Model_Mysql4_Abstract
4
+ {
5
+ public function _construct()
6
+ {
7
+ $this->_init('nypwidget/deal_usage', '');
8
+ }
9
+
10
+ /**
11
+ * @param Mage_Sales_Model_Quote $quote
12
+ * @return Array An Array of Deal models.
13
+ */
14
+ public function getDealsUsedByQuote(
15
+ Mage_Sales_Model_Quote $quote
16
+ )
17
+ {
18
+ $adapter = $this->_getReadAdapter();
19
+ $select = $adapter->select();
20
+
21
+ $select
22
+ ->from($this->getMainTable(), array('deal_id'))
23
+ ->where(
24
+ 'quote_id = ?',
25
+ $quote->getId()
26
+ );
27
+
28
+ $ids = $adapter->fetchCol($select);
29
+
30
+ $collection = Mage::getModel('nypwidget/deal')
31
+ ->getCollection()
32
+ ->addFieldToFilter(
33
+ 'deal_id',
34
+ array('in' => $ids)
35
+ );
36
+
37
+ return $collection->getItems();
38
+ }
39
+
40
+ /**
41
+ * @param PriceWaiter_NYPWidget_Model_Deal|string $deal
42
+ * @return Array IDs of quotes that have used $deal.
43
+ */
44
+ public function getQuoteIdsUsingDeal(
45
+ $deal
46
+ )
47
+ {
48
+ $adapter = $this->_getReadAdapter();
49
+ $select = $adapter->select();
50
+
51
+ $select
52
+ ->from($this->getMainTable(), array('quote_id'))
53
+ ->where(
54
+ 'deal_id = ?',
55
+ is_object($deal) ? $deal->getId() : $deal
56
+ );
57
+
58
+ $result = $adapter->fetchCol($select);
59
+ return $result;
60
+ }
61
+
62
+ /**
63
+ * Looks up the (unique) ids of orders that use any of the given deals.
64
+ * @param array $dealIds
65
+ * @return array Array of order ids
66
+ */
67
+ public function getOrderIdsForDealIds(array $dealIds)
68
+ {
69
+ $dealIds = array_unique($dealIds);
70
+
71
+ if (empty($dealIds)) {
72
+ return array();
73
+ }
74
+
75
+ $adapter = $this->_getReadAdapter();
76
+ $select = $adapter->select();
77
+
78
+ $select
79
+ ->distinct()
80
+ ->from(
81
+ array(
82
+ 'deal' => $this->getTable('nypwidget/deal'),
83
+ ),
84
+ array('order_id')
85
+ )
86
+ ->join(
87
+ array('order' => $this->getTable('sales/order')),
88
+ 'order.entity_id = deal.order_id',
89
+ array()
90
+ )
91
+ // We're *really* not concerned with orders in certain states.
92
+ ->where('order.state NOT IN (?)', array(
93
+ Mage_Sales_Model_Order::STATE_CANCELED,
94
+ ))
95
+ ->where('deal.deal_id IN (?)', $dealIds);
96
+
97
+ return $adapter->fetchCol($select);
98
+ }
99
+
100
+ /**
101
+ * Given an array of order entity_ids, returns an array
102
+ * array(
103
+ * 'order_id_1' => array('deal_id_1', 'deal_id_2')
104
+ * )
105
+ * Describing the deals applied to the orders.
106
+ * Any orders without deals will be excluded from the results.
107
+ * @param array $orderIds
108
+ * @return array
109
+ */
110
+ public function getDealUsageForOrderIds(array $orderIds)
111
+ {
112
+ $orderIds = array_unique($orderIds);
113
+
114
+ $adapter = $this->_getReadAdapter();
115
+ $select = $adapter->select();
116
+
117
+ $select
118
+ ->from($this->getTable('nypwidget/deal'), array('deal_id', 'order_id'))
119
+ ->where('order_id in (?)', $orderIds);
120
+
121
+ $rows = $adapter->fetchAll($select);
122
+ $result = array();
123
+
124
+ foreach($rows as $row) {
125
+ $orderId = $row['order_id'];
126
+ $dealId = $row['deal_id'];
127
+
128
+ if (!isset($result[$orderId])) {
129
+ $result[$orderId] = array();
130
+ }
131
+ $result[$orderId][] = $dealId;
132
+ }
133
+
134
+ return $result;
135
+ }
136
+
137
+ /**
138
+ * @param array $dealIds
139
+ * @return array An array of arrays, each with 'order' and 'dealIds' keys.
140
+ */
141
+ public function getOrdersAndDealUsageForDealIds(array $dealIds)
142
+ {
143
+ // This method does all the work needed to turn an abitrary set of deal
144
+ // ids into order + deal usage information in a minimal number of DB queries:
145
+
146
+ // 1. Translate $dealIds into an array of order ids
147
+ $orderIds = $this->getOrderIdsForDealIds($dealIds);
148
+
149
+ // 2. Look up all deal usage for those order ids (including those not in $dealIds)
150
+ $dealUsage = $this->getDealUsageForOrderIds($orderIds);
151
+
152
+ // 3. Query for order models by id
153
+ $orders = Mage::getModel('sales/order')
154
+ ->getCollection()
155
+ ->addFieldToFilter('entity_id', array('in' => $orderIds))
156
+ ->getItems();
157
+
158
+ // Finally, stitch together order models with arrays of deals
159
+ $result = array();
160
+
161
+ /** @var Mage_Sales_Model_Order $order */
162
+ foreach($orders as $order) {
163
+ $result[] = array(
164
+ 'order' => $order,
165
+ 'dealIds' => $dealUsage[$order->getId()],
166
+ );
167
+ }
168
+
169
+ return $result;
170
+ }
171
+
172
+ /**
173
+ * Records a set of zero or more Deals used on a quote.
174
+ * Previous links between Deal <-> Quote are overwritten.
175
+ *
176
+ * @param Mage_Sales_Model_Quote $quote
177
+ * @param array $deals Array of Deal models or Deal ids.
178
+ */
179
+ public function recordDealUsageForQuote(
180
+ Mage_Sales_Model_Quote $quote,
181
+ array $deals
182
+ )
183
+ {
184
+ $quoteId = $quote->getId();
185
+
186
+ // In normal circumstances, $quote will have an ID.
187
+ // There are *some* times when it is technically possible for it
188
+ // not to (like if someone is fiddling with the cart in code).
189
+ if (!$quoteId) {
190
+ // We can't record usage without a quote id.
191
+ return;
192
+ }
193
+
194
+ $rowsToInsert = array();
195
+ foreach ($deals as $deal) {
196
+ $rowsToInsert[] = array(
197
+ 'deal_id' => is_object($deal) ? $deal->getId() : $deal,
198
+ 'quote_id' => $quoteId,
199
+ );
200
+ }
201
+
202
+ $adapter = $this->_getWriteAdapter();
203
+ $adapter->beginTransaction();
204
+ try
205
+ {
206
+ $adapter->delete(
207
+ $this->getMainTable(),
208
+ array(
209
+ 'quote_id = ?' => $quote->getId(),
210
+ )
211
+ );
212
+
213
+ if (!empty($rowsToInsert)) {
214
+ $adapter->insertArray(
215
+ $this->getMainTable(),
216
+ array('deal_id', 'quote_id'),
217
+ $rowsToInsert
218
+ );
219
+ }
220
+ } catch (Exception $ex)
221
+ {
222
+ $adapter->rollBack();
223
+ throw $ex;
224
+ }
225
+
226
+ $adapter->commit();
227
+ }
228
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Observer.php CHANGED
@@ -30,4 +30,52 @@ class PriceWaiter_NYPWidget_Model_Observer
30
 
31
  return true;
32
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  }
30
 
31
  return true;
32
  }
33
+
34
+ /**
35
+ * Called when the customer logs out.
36
+ * @param Varien_Event_Observer $observer
37
+ */
38
+ public function handleCustomerLogout(Varien_Event_Observer $observer)
39
+ {
40
+ // Reset the PriceWaiter Buyer ID in session
41
+ $session = Mage::getSingleton('nypwidget/session');
42
+ $session->reset();
43
+ }
44
+
45
+ /**
46
+ * Called when a quote is converted into an order.
47
+ * Used to tie the order to any PriceWaiter Deals used to establish the pricing.
48
+ *
49
+ * @param Varien_Event_Observer $observer
50
+ */
51
+ public function tieOrderToPriceWaiterDeals(Varien_Event_Observer $observer)
52
+ {
53
+ $event = $observer->getEvent();
54
+ $quote = $event->getQuote();
55
+ $order = $event->getOrder();
56
+
57
+ try
58
+ {
59
+ $res = Mage::getResourceModel('nypwidget/deal_usage');
60
+ $deals = $res->getDealsUsedByQuote($quote);
61
+
62
+ if (empty($deals)) {
63
+ return;
64
+ }
65
+
66
+ $transaction = Mage::getModel('core/resource_transaction');
67
+
68
+ foreach($deals as $deal) {
69
+ $deal->setOrderId($order->getId());
70
+ $transaction->addObject($deal);
71
+ }
72
+
73
+ $transaction->save();
74
+ }
75
+ catch (Exception $ex)
76
+ {
77
+ // Never let our code prevent an order from being processed.
78
+ Mage::logException($ex);
79
+ }
80
+ }
81
  }
app/code/community/PriceWaiter/NYPWidget/Model/Offer/Item.php ADDED
@@ -0,0 +1,501 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Represents a single item in a PriceWaiter Offer / Deal.
5
+ *
6
+ * This item can map to more than 1 Magento product (for example, if the offer
7
+ * was for a bundle product).
8
+ */
9
+ class PriceWaiter_NYPWidget_Model_Offer_Item
10
+ {
11
+ /**
12
+ * Key in metadata that holds the serialized add to cart form.
13
+ */
14
+ const ADD_TO_CART_FORM_METADATA_KEY = '_magento_product_configuration';
15
+
16
+ /**
17
+ * The absolute minimum quantity we support for things. Numbers lower
18
+ * than this will be set to this value.
19
+ */
20
+ const MINIMUM_QUANTITY = 1;
21
+
22
+ /**
23
+ * @var Object
24
+ */
25
+ private $_data;
26
+
27
+ /**
28
+ * @internal Cached array representation of item metadata.
29
+ * @var Array
30
+ */
31
+ private $_metadataArray = null;
32
+
33
+ /**
34
+ * @internal Cached array of product option data.
35
+ * @var Array
36
+ */
37
+ private $_optionsArray = null;
38
+
39
+ /**
40
+ * ID of the Magento store this item is for.
41
+ * @var integer
42
+ */
43
+ protected $_storeId;
44
+
45
+ /**
46
+ * @param Object $data
47
+ * @param Object|integer $store Store (or just ID) this item is for.
48
+ */
49
+ public function __construct($data = null, $store = null)
50
+ {
51
+ $this->_data = $data ? $data : array();
52
+
53
+ if ($store === null) {
54
+ $store = Mage::app()->getStore();
55
+ }
56
+
57
+ if ($store instanceof Mage_Core_Model_Store) {
58
+ $this->_storeId = $store->getId();
59
+ } else if (is_numeric($store)) {
60
+ $this->_storeId = $store;
61
+ } else {
62
+ throw new InvalidArgumentException(__CLASS__ . ' constructor requires store.');
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Adds the product(s) contained in this Offer item to the given quote.
68
+ * @param Mage_Sales_Model_Quote $quote
69
+ * @return Mage_Sales_Model_Quote_Item The item added.
70
+ */
71
+ public function addToQuote(Mage_Sales_Model_Quote $quote)
72
+ {
73
+ list($product, $addToCartForm, $handler) = $this->loadProduct();
74
+
75
+ return $handler->addProductToQuote(
76
+ $quote,
77
+ $product,
78
+ $addToCartForm,
79
+ $this->getMaximumQuantity()
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Checks that the product(s) represented by this Offer Item is present
85
+ * (in at least the minimum qty) in the given quote.
86
+ *
87
+ * If not present, the product(s) is/are added to the quote.
88
+ * @param Mage_Sales_Model_Quote $quote
89
+ * @return Mage_Sales_Model_Quote_Item The quote item found or added.
90
+ */
91
+ public function ensurePresentInQuote(Mage_Sales_Model_Quote $quote)
92
+ {
93
+ $items = $quote->getAllItems();
94
+ $item = $this->findQuoteItem($items);
95
+
96
+ try
97
+ {
98
+ if ($item) {
99
+ $minOk = $item->getQty() >= $this->getMinimumQuantity();
100
+ $maxOk = $item->getQty() <= $this->getMaximumQuantity();
101
+
102
+ if ($minOk && $maxOk) {
103
+ // Already in quote, no update required.
104
+ return $item;
105
+ } else if (!$minOk) {
106
+ // Need to bring quantity *up* to minimum
107
+ $item->setQty($this->getMinimumQuantity());
108
+ } else if (!$maxOk) {
109
+ // Need to bring quantity *down* to maximum
110
+ $item->setQty($this->getMaximumQuantity());
111
+ }
112
+ } else {
113
+ // Item is not even present in quote. So add it.
114
+ $item = $this->addToQuote($quote);
115
+ }
116
+
117
+ return $item;
118
+ }
119
+ catch (Exception $ex)
120
+ {
121
+ $translatedEx = PriceWaiter_NYPWidget_Exception_Abstract::translateMagentoException($ex);
122
+ throw $translatedEx;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Given a set of Mage_Sales_Model_Quote_Item instances, returns the *1*
128
+ * that (when also considering its children) contains the product(s)
129
+ * contained in this PriceWaiter order.
130
+ *
131
+ * NOTE: This does *not* consider min and max quantity--the result is based
132
+ * solely on whether the product in the quote item is the same product
133
+ * we're considering.
134
+ *
135
+ * If no matching quote item is found, returns false.
136
+ * @param array $items
137
+ * @return Mage_Sales_Model_Quote_Item|false
138
+ */
139
+ public function findQuoteItem(array $items)
140
+ {
141
+ list($product, $cart, $handler) = $this->loadProduct();
142
+
143
+ // $handler handles trickiness associated with bundle products etc.
144
+ $item = $handler->findQuoteItem($product, $cart, $items);
145
+
146
+ if (!$item) {
147
+ return false;
148
+ }
149
+
150
+ return $item;
151
+ }
152
+
153
+ /**
154
+ * Attempts to read a value from the serialized Magento Add to Cart form
155
+ * stored in the Offer metadata. Uses parse_str internally, so array-style
156
+ * keys[] are expanded into arrays.
157
+ * If called without arguments, returns the full contents of the
158
+ * add to cart form as an array.
159
+ * @param String $key
160
+ * @param Mixed $default
161
+ * @return Mixed
162
+ */
163
+ public function getAddToCartForm($key = null, $default = null)
164
+ {
165
+ $values = $this->getAllAddToCartFormValues();
166
+
167
+ // Allow getAddToCartForm() to return full form. (like getMetadata()).
168
+ if (func_num_args() === 0) {
169
+ return $values;
170
+ }
171
+
172
+ return array_key_exists($key, $values) ?
173
+ $values[$key] :
174
+ $default;
175
+ }
176
+
177
+ /**
178
+ * @return Number Amount per item offered (as a decimal).
179
+ */
180
+ public function getAmountPerItem()
181
+ {
182
+ return doubleval($this->get('amount_per_item.value', 0));
183
+ }
184
+
185
+ /**
186
+ * @return String The 3-letter ISO currency code the offer was made in.
187
+ */
188
+ public function getCurrencyCode()
189
+ {
190
+ return $this->get('currency');
191
+ }
192
+
193
+ /**
194
+ * @return PriceWaiter_NYPWidget_Model_Offer_Inventory
195
+ */
196
+ public function getInventory()
197
+ {
198
+ list($product, $cart, $handler) = $this->loadProduct();
199
+ return $handler->getInventory($product, $cart);
200
+ }
201
+
202
+ /**
203
+ * @return PriceWaiter_NYPWidget_Model_Offer_Pricing
204
+ */
205
+ public function getPricing()
206
+ {
207
+ list($product, $cart, $handler) = $this->loadProduct();
208
+ return $handler->getPricing($product, $cart);
209
+ }
210
+
211
+ /**
212
+ * @return Integer Maximum quantity of this item that can be purchased.
213
+ */
214
+ public function getMaximumQuantity()
215
+ {
216
+ $max = $this->get('quantity.max', null);
217
+
218
+ if (!is_numeric($max)) {
219
+ return $this->getMinimumQuantity();
220
+ }
221
+
222
+ return max($max, $this->getMinimumQuantity());
223
+ }
224
+
225
+ /**
226
+ * Returns the URL of the Magento product referenced by this item.
227
+ * @throws PriceWaiter_NYPWidget_Exception_Product_NotFound
228
+ * @return String
229
+ */
230
+ public function getMagentoProductUrl()
231
+ {
232
+ list($product) = $this->loadProduct();
233
+ return $product->getProductUrl();
234
+ }
235
+
236
+ /**
237
+ * Attempts to read a string value from the Offer's metadata.
238
+ * If you don't pass in any arguments, returns the full metadata array.
239
+ * @param String $key
240
+ * @param Mixed $default
241
+ * @return Mixed
242
+ */
243
+ public function getMetadata($key = null, $default = null)
244
+ {
245
+ // Read metadata out of _data and into a standard array first
246
+ if (is_null($this->_metadataArray)) {
247
+
248
+ $this->_metadataArray = array();
249
+
250
+ $m = $this->get('metadata');
251
+
252
+ if ($m) {
253
+ foreach ($m as $k => $v) {
254
+ $this->_metadataArray[$k] = $v;
255
+ }
256
+ }
257
+ }
258
+
259
+ if (func_num_args() === 0) {
260
+ // No args = return all metadata
261
+ return $this->_metadataArray;
262
+ }
263
+
264
+ return array_key_exists($key, $this->_metadataArray) ?
265
+ $this->_metadataArray[$key] :
266
+ $default;
267
+ }
268
+
269
+ /**
270
+ * @return Integer The minimum quantity of this item that must be purchased.
271
+ */
272
+ public function getMinimumQuantity()
273
+ {
274
+ $min = $this->get('quantity.min', null);
275
+
276
+ if (is_null($min)) {
277
+ // Allow specifying min+max with a single number
278
+ $min = $this->get('quantity');
279
+ }
280
+
281
+ if (!is_numeric($min)) {
282
+ return self::MINIMUM_QUANTITY;
283
+ }
284
+
285
+ return max(self::MINIMUM_QUANTITY, $min);
286
+ }
287
+
288
+ /**
289
+ * @return String The reported name of the product.
290
+ */
291
+ public function getProductName()
292
+ {
293
+ return (string)$this->get('product.name');
294
+ }
295
+
296
+ /**
297
+ * @return Array An associative array whose keys are product option names and values are the associated values.
298
+ */
299
+ public function getProductOptions()
300
+ {
301
+ if (!is_null($this->_optionsArray)) {
302
+ return $this->_optionsArray;
303
+ }
304
+
305
+ $this->_optionsArray = array();
306
+
307
+ $options = $this->get('product.options', null);
308
+ if ($options) {
309
+ foreach ($options as $o) {
310
+ $name = (string)self::_get($o, 'name', '');
311
+ $value = (string)self::_get($o, 'value', '');
312
+ $this->_optionsArray[$name] = $value;
313
+ }
314
+ }
315
+
316
+ return $this->_optionsArray;
317
+ }
318
+
319
+ /**
320
+ * @return String The SKU of the product on the PriceWaiter offer.
321
+ */
322
+ public function getProductSku()
323
+ {
324
+ return (string)$this->get('product.sku');
325
+ }
326
+
327
+ /**
328
+ * @return Integer ID of the store this Offer Item is meant for.
329
+ */
330
+ public function getStoreId()
331
+ {
332
+ return $this->_storeId;
333
+ }
334
+
335
+ /**
336
+ * @return Boolean
337
+ */
338
+ public function quoteItemMeetsQuantityRequirements(Mage_Sales_Model_Quote_Item $item)
339
+ {
340
+ $qty = $item->getQty();
341
+ return ($qty >= $this->getMinimumQuantity()) && ($qty <= $this->getMaximumQuantity());
342
+ }
343
+
344
+ /**
345
+ * @param Array $addToCartForm
346
+ * @return PriceWaiter_NYPWidget_Model_Offer_Item A clone of this item with a new add to cart form.
347
+ */
348
+ public function withAddToCartForm(array $addToCartForm)
349
+ {
350
+ $metadata = $this->getMetadata();
351
+ $metadata[self::ADD_TO_CART_FORM_METADATA_KEY] = http_build_query($addToCartForm, '', '&');
352
+
353
+ $newData = $this->_data;
354
+ $newData['metadata'] = $metadata;
355
+
356
+ return new self($newData, $this->_storeId);
357
+ }
358
+
359
+ /**
360
+ * @internal
361
+ */
362
+ protected function get($key, $default = null)
363
+ {
364
+ return self::_get($this->_data, $key, $default);
365
+ }
366
+
367
+ /**
368
+ * @internal
369
+ * @return Array The full add to cart form.
370
+ */
371
+ protected function getAllAddToCartFormValues()
372
+ {
373
+ // Magento platform JS serializes the add to cart form into metadata.
374
+ $serializedAddToCartForm = $this->getMetadata(self::ADD_TO_CART_FORM_METADATA_KEY, null);
375
+
376
+ if (is_null($serializedAddToCartForm)) {
377
+ return array();
378
+ }
379
+
380
+ // Ok. $serializedAddToCartForm is an url-encoded form string like
381
+ // 'foo=bar&baz=bat'
382
+
383
+ $addToCartFormValues = array();
384
+ parse_str($serializedAddToCartForm, $addToCartFormValues);
385
+
386
+ // HACK: Ok, for some reason we are currently double-encoding the contents
387
+ // of this key--platform JS is taking the serialized form string and
388
+ // passing it through encodeURIComponent(). Here we detect that and try again.
389
+ if (count($addToCartFormValues) === 1) {
390
+
391
+ // $values *might* look something like
392
+ // `array('field=value&field2=value' => '')`
393
+ // which would indicate it was double-encoded.
394
+
395
+ $keys = array_keys($addToCartFormValues);
396
+ $values = array_values($addToCartFormValues);
397
+
398
+ $looksDoubleEncoded = (
399
+ $values[0] === '' &&
400
+ strpos($keys[0], '&') !== false
401
+ );
402
+
403
+ if ($looksDoubleEncoded) {
404
+ $addToCartFormValues = array();
405
+ parse_str($keys[0], $addToCartFormValues);
406
+ }
407
+ }
408
+
409
+ return $addToCartFormValues;
410
+ }
411
+
412
+ /**
413
+ * Returns a separate class that thinks about the dirty details of Magento
414
+ * products.
415
+ * @param Mage_Catalog_Model_Product $product
416
+ * @return PriceWaiter_NYPWidget_Model_Offer_Item_Handler
417
+ */
418
+ protected function getHandlerForProduct(Mage_Catalog_Model_Product $product)
419
+ {
420
+ $class = 'nypwidget/offer_item_handler';
421
+ $handler = Mage::getSingleton($class);
422
+ return $handler;
423
+ }
424
+
425
+ /**
426
+ * Returns an array with 3 elements:
427
+ *
428
+ * 1. The main Magento product this item refers to.
429
+ * 2. A Varien_Object of add to cart data
430
+ * 3. A handler instance to use to query for more information
431
+ *
432
+ * @return Array
433
+ * @throws PriceWaiter_NYPWidget_Exception_Product_NotFound
434
+ */
435
+ protected function loadProduct()
436
+ {
437
+ // This method resembles what CartController::_initProduct does when
438
+ // reconstituting a product instance from add to cart data.
439
+
440
+ $addToCartForm = $this->getAddToCartForm();
441
+ $id = isset($addToCartForm['product']) ? $addToCartForm['product'] : null;
442
+
443
+ if (!$id) {
444
+ throw new PriceWaiter_NYPWidget_Exception_Product_NotFound('product not specified in add to cart form.');
445
+ }
446
+
447
+ $product = Mage::getModel('catalog/product')
448
+ ->setStoreId($this->getStoreId())
449
+ ->load($id);
450
+
451
+ if (!$product->getId()) {
452
+ throw new PriceWaiter_NYPWidget_Exception_Product_NotFound("Product with id '{$id}' not found.");
453
+ }
454
+
455
+ $handler = $this->getHandlerForProduct($product);
456
+
457
+ $addToCartForm = new Varien_Object($addToCartForm);
458
+
459
+ // Slight HACK: Ensure that we're always considering 1 of the main product at a time.
460
+ // We support > 1 qty for *child* products (such as items in a bundle),
461
+ // but don't want quantities to affect price calculations for parent products.
462
+ if ($addToCartForm->hasQty()) {
463
+ $addToCartForm->setQty(1);
464
+ }
465
+
466
+ return array(
467
+ $product,
468
+ $addToCartForm,
469
+ $handler,
470
+ );
471
+ }
472
+
473
+ /**
474
+ * @internal Reads dot.separated.keys off an object or array.
475
+ */
476
+ private static function _get($obj, $key, $default = null)
477
+ {
478
+ $keyParts = is_array($key) ? $key : explode('.', $key);
479
+ $k = array_shift($keyParts);
480
+
481
+ if (is_object($obj)) {
482
+ if (isset($obj->$k)) {
483
+ if (count($keyParts) === 0) {
484
+ return $obj->$k;
485
+ } else {
486
+ return self::_get($obj->$k, $keyParts, $default);
487
+ }
488
+ }
489
+ } else if (is_array($obj)) {
490
+ if (array_key_exists($k, $obj)) {
491
+ if (count($keyParts) === 0) {
492
+ return $obj[$k];
493
+ } else {
494
+ return self::_get($obj[$k], $keyParts, $default);
495
+ }
496
+ }
497
+ }
498
+
499
+ return $default;
500
+ }
501
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Offer/Item/Handler.php ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class responsible for actually touching Magento products to do things
5
+ * for PriceWaiter offers. Here be dragons.
6
+ */
7
+ class PriceWaiter_NYPWidget_Model_Offer_Item_Handler
8
+ {
9
+ /**
10
+ * Adds the given product to the given quote.
11
+ * @param Mage_Sales_Model_Quote $quote
12
+ * @param Mage_Catalog_Model_Product $product
13
+ * @param Varien_Object $addToCartForm
14
+ * @param Integer $qty
15
+ * @return Mage_Sales_Model_Quote_Item
16
+ */
17
+ public function addProductToQuote(
18
+ Mage_Sales_Model_Quote $quote,
19
+ Mage_Catalog_Model_Product $product,
20
+ Varien_Object $addToCartForm,
21
+ $qty
22
+ )
23
+ {
24
+ $addToCartForm = clone($addToCartForm);
25
+ $addToCartForm->setQty($qty);
26
+
27
+ return $quote->addProduct($product, $addToCartForm);
28
+ }
29
+
30
+ /**
31
+ * @param Mage_Catalog_Model_Product $product
32
+ * @param Varien_Object $addToCartForm
33
+ * @param array $quoteItems
34
+ * @return Mage_Sales_Model_Quote_Item|false
35
+ */
36
+ public function findQuoteItem(
37
+ Mage_Catalog_Model_Product $product,
38
+ Varien_Object $addToCartForm,
39
+ array $quoteItems
40
+ )
41
+ {
42
+ $products = $this->getConfiguredProducts($product, $addToCartForm);
43
+ list($parent, $children) = $this->splitParentAndChildProducts($products);
44
+
45
+ foreach ($quoteItems as $quoteItem) {
46
+ $matches = $this->quoteItemMatches(
47
+ $quoteItem,
48
+ $parent,
49
+ $children
50
+ );
51
+
52
+ if ($matches) {
53
+ return $quoteItem;
54
+ }
55
+ }
56
+
57
+ return false;
58
+ }
59
+
60
+ /**
61
+ * Returns a structure describing the inventory tracking for this product.
62
+ * @param Mage_Catalog_Model_Product $product
63
+ * @param Varien_Object $addToCartForm
64
+ * @return PriceWaiter_NYPWidget_Model_Offer_Item_Inventory
65
+ */
66
+ public function getInventory(
67
+ Mage_Catalog_Model_Product $product,
68
+ Varien_Object $addToCartForm
69
+ )
70
+ {
71
+ $products = $this->getConfiguredProducts($product, $addToCartForm);
72
+ return Mage::getModel('nypwidget/offer_item_inventory', $products);
73
+ }
74
+
75
+ /**
76
+ * @return PriceWaiter_NYPWidget_Model_Offer_Item_Pricing
77
+ */
78
+ public function getPricing(
79
+ Mage_Catalog_Model_Product $product,
80
+ Varien_Object $addToCartForm
81
+ )
82
+ {
83
+ $products = $this->getConfiguredProducts($product, $addToCartForm);
84
+
85
+ $pricing = Mage::getModel(
86
+ 'nypwidget/offer_item_pricing',
87
+ $products
88
+ );
89
+
90
+ return $pricing;
91
+ }
92
+
93
+ /**
94
+ * Returns a set of *configured* products for the given parent product / cart data combo.
95
+ * @param Mage_Catalog_Model_Product $product
96
+ * @param Varien_Object $cart
97
+ * @return array
98
+ */
99
+ public function getConfiguredProducts(Mage_Catalog_Model_Product $product, Varien_Object $addToCartForm)
100
+ {
101
+ $type = $product->getTypeInstance();
102
+ $products = $type->prepareForCart($addToCartForm);
103
+
104
+ if (is_string($products)) {
105
+ // Magento communicates "add to cart" errors by returning a string here.
106
+ // Most likely, $addToCartForm does not contain data for all required
107
+ // product options.
108
+ $id = $product->getId();
109
+
110
+ throw new PriceWaiter_NYPWidget_Exception_Product_Invalid(
111
+ "Error preparing product (id: {$id}): {$products}"
112
+ );
113
+ }
114
+
115
+ return $products;
116
+ }
117
+
118
+ /**
119
+ * @param Mage_Catalog_Model_Product $product
120
+ * @return Array a simple array of [custom option id => value] for $product
121
+ */
122
+ protected function buildCustomOptionArray(Mage_Catalog_Model_Product $product)
123
+ {
124
+ $result = array();
125
+
126
+ /** @var Mage_Catalog_Model_Product_Option $opt */
127
+ foreach($product->getOptions() as $opt) {
128
+ $code = 'option_' . $opt->getId();
129
+ $customOption = $product->getCustomOption($code);
130
+
131
+ if (!$customOption) {
132
+ continue;
133
+ }
134
+
135
+ $result[$opt->getId()] = $customOption->getValue();
136
+ }
137
+
138
+ return $result;
139
+ }
140
+
141
+ /**
142
+ * Tests that the products referred to by $childQuoteItems exactly
143
+ * matches the products in $childProducts.
144
+ * Used for matching quote items for non-simple products that
145
+ * exploit quote item hierarchy.
146
+ * @param array $childQuoteItems
147
+ * @param array $childProducts
148
+ * @return boolean
149
+ */
150
+ protected function childQuoteItemsMatchChildProducts(
151
+ array $childQuoteItems,
152
+ array $childProducts
153
+ )
154
+ {
155
+ if (count($childQuoteItems) !== count($childProducts)) {
156
+ return false;
157
+ }
158
+
159
+ // Compare the product ids / quantities of the given quote items
160
+ // with the product ids / quantities of the products.
161
+ // This additional quantity check prevents matching quote items
162
+ // for bundle products where the same products are used but
163
+ // quantities differ.
164
+
165
+ $productIds = array();
166
+ foreach ($childProducts as $product) {
167
+ $id = strval($product->getId());
168
+ $productIds[$id] = strval($product->getCartQty());
169
+ }
170
+
171
+ $quoteItemProductIds = array();
172
+ foreach ($childQuoteItems as $quoteItem) {
173
+ $id = strval($quoteItem->getProductId());
174
+ $quoteItemProductIds[$id] = strval($quoteItem->getQty());
175
+ }
176
+
177
+ $diff = array_diff_assoc($productIds, $quoteItemProductIds);
178
+
179
+ return empty($diff);
180
+ }
181
+
182
+ protected function productCustomOptionsMatch(
183
+ Mage_Catalog_Model_Product $productA,
184
+ Mage_Catalog_Model_Product $productB
185
+ )
186
+ {
187
+ $customOptionsA = $this->buildCustomOptionArray($productA);
188
+ $customOptionsB = $this->buildCustomOptionArray($productB);
189
+
190
+ if (count($customOptionsA) !== count($customOptionsB)) {
191
+ return false;
192
+ }
193
+
194
+ $diff = array_diff_assoc($customOptionsA, $customOptionsB);
195
+ return empty($diff);
196
+ }
197
+
198
+ /**
199
+ * @param Mage_Sales_Model_Quote_Item $quoteItem
200
+ * @param Mage_Catalog_Model_Product $parent Parent product
201
+ * @param array $children Child products
202
+ * @return Boolean
203
+ */
204
+ protected function quoteItemMatches(
205
+ Mage_Sales_Model_Quote_Item $quoteItem,
206
+ Mage_Catalog_Model_Product $parent,
207
+ array $children
208
+ )
209
+ {
210
+ // We only match against parent quote items.
211
+ // This prevents things like having PW deals apply to
212
+ // products *inside* bundles.
213
+ $isParent = !$quoteItem->getParentItemId();
214
+ if (!$isParent) {
215
+ return false;
216
+ }
217
+
218
+ $isSameProduct = $quoteItem->getProductId() == $parent->getId();
219
+ if (!$isSameProduct) {
220
+ return false;
221
+ }
222
+
223
+ $customOptionsMatch = $this->productCustomOptionsMatch(
224
+ $parent,
225
+ $quoteItem->getProduct()
226
+ );
227
+
228
+ if (!$customOptionsMatch) {
229
+ return false;
230
+ }
231
+
232
+ // Ok, This *parent* quote item matches well enough.
233
+ // But we also need to verify that any children of this quote item
234
+ // match the incoming child products (this is for configurable +
235
+ // bundle support).
236
+
237
+ $childQuoteItems = $quoteItem->getChildren();
238
+ return $this->childQuoteItemsMatchChildProducts($childQuoteItems, $children);
239
+ }
240
+
241
+ /**
242
+ * Splits an array of products into a single parent product and 0 or more
243
+ * child products.
244
+ * @param array $products
245
+ * @return array
246
+ */
247
+ protected function splitParentAndChildProducts(array $products)
248
+ {
249
+ $parent = null;
250
+ $children = array();
251
+
252
+ /** @var Mage_Catalog_Model_Product $product */
253
+ foreach($products as $product) {
254
+ if ($product->getParentProductId()) {
255
+ $children[] = $product;
256
+ continue;
257
+ }
258
+ $parent = $product;
259
+ }
260
+
261
+ return array($parent, $children);
262
+ }
263
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Offer/Item/Inventory.php ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Layer that adapts the way Magento thinks about inventory to the way
5
+ * PriceWaiter thinks about inventory.
6
+ */
7
+ class PriceWaiter_NYPWidget_Model_Offer_Item_Inventory
8
+ {
9
+ protected $_products;
10
+ protected $_productsWithStockItems = null;
11
+
12
+ public function __construct(array $products)
13
+ {
14
+ $this->_products = $products;
15
+ }
16
+
17
+ /**
18
+ * @return Boolean Whether backorders are allowed for this item.
19
+ */
20
+ public function canBackorder()
21
+ {
22
+ // NOTE: Any *one* item not being backorderable means we can't consider
23
+ // the group backorderable.
24
+
25
+ foreach($this->getProductsWithStockItems() as $p) {
26
+ list($product, $stockItem) = $p;
27
+
28
+ // Technically getBackorders() is a flag with multiple states but
29
+ // BACKORDERS_NO = 0 so this works.
30
+ if (!$stockItem->getBackorders()) {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ return true;
36
+ }
37
+
38
+ /**
39
+ * @return Integer|false The current # in stock (or false if unknown).
40
+ */
41
+ public function getStock()
42
+ {
43
+ $result = false;
44
+
45
+ foreach($this->getProductsWithStockItems() as $p) {
46
+ list($product, $stockItem) = $p;
47
+
48
+ if (!$stockItem->getManageStock()) {
49
+ // Not tracking stock for this item, so ignore.
50
+ continue;
51
+ }
52
+
53
+ // Since we are considering the products as a group, return the
54
+ // *minimum* quantity available.
55
+ $qty = $this->getQty($product, $stockItem);
56
+
57
+ if ($result === false || $qty < $result) {
58
+ $result = $qty;
59
+ }
60
+ }
61
+
62
+ return $result === false ? $result : intval($result);
63
+ }
64
+
65
+ /**
66
+ * @param Mage_Catalog_Model_Product $product
67
+ * @param Mage_CatalogInventory_Model_Stock_Item $stockItem
68
+ * @return Integer the # of the product available.
69
+ */
70
+ protected function getQty(
71
+ Mage_Catalog_Model_Product $product,
72
+ Mage_CatalogInventory_Model_Stock_Item $stockItem
73
+ )
74
+ {
75
+ // For products that are part of a bundle, we have to
76
+ // return how many are available in that increment.
77
+ //
78
+ // So if bundle contains 2 x Shirt, and Shirt has 100 left,
79
+ // that means the effective quantity for the Shirt in the bundle
80
+ // is 50 (100 / 2).
81
+ $increment = $product->getCartQty();
82
+
83
+ if ($increment > 1) {
84
+ return floor($stockItem->getQty() / $increment);
85
+ }
86
+
87
+ // Ordinarily, though, we just use the stock item's quantity.
88
+ return $stockItem->getQty();
89
+ }
90
+
91
+ /**
92
+ * @return array The set of stock items for the products being considered.
93
+ */
94
+ protected function getProductsWithStockItems()
95
+ {
96
+ if ($this->_productsWithStockItems !== null) {
97
+ return $this->_productsWithStockItems;
98
+ }
99
+
100
+ $this->_productsWithStockItems = array();
101
+
102
+ // If we have any child products, only return stock items for those.
103
+ // Otherwise (for e.g. grouped products), return stock items for *all* products.
104
+ $haveAnyChildren = false;
105
+
106
+ foreach($this->_products as $product) {
107
+ $isChild = !!$product->getParentProductId();
108
+ if ($isChild) {
109
+ $haveAnyChildren = true;
110
+ break;
111
+ }
112
+ }
113
+
114
+ foreach($this->_products as $product) {
115
+ $isChild = !!$product->getParentProductId();
116
+
117
+ if ($haveAnyChildren && !$isChild) {
118
+ // Ignore parents for inventory purposes
119
+ continue;
120
+ }
121
+
122
+ $this->_productsWithStockItems[] = array($product, $product->getStockItem());
123
+ }
124
+
125
+ return $this->_productsWithStockItems;
126
+ }
127
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Offer/Item/Pricing.php ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class that adapts how Magento thinks about prices to how PriceWaiter does.
5
+ */
6
+ class PriceWaiter_NYPWidget_Model_Offer_Item_Pricing
7
+ {
8
+ protected $_products;
9
+
10
+ public function __construct(array $products)
11
+ {
12
+ $this->_products = $products;
13
+ }
14
+
15
+ public function getCost()
16
+ {
17
+ return $this->calculatePrice(
18
+ $this->getProductsForOtherPriceFields(),
19
+ 'getCostForProduct'
20
+ );
21
+ }
22
+
23
+ /**
24
+ * @return Mage_Directory_Model_Currency
25
+ */
26
+ public function getCurrency()
27
+ {
28
+ foreach($this->_products as $product) {
29
+ $store = Mage::app()->getStore($product->getStoreId());
30
+ return $store->getDefaultCurrency();
31
+ }
32
+
33
+ throw new RuntimeException("Cannot determine currency.");
34
+ }
35
+
36
+ /**
37
+ * @return string 3-character currency code.
38
+ */
39
+ public function getCurrencyCode()
40
+ {
41
+ $currency = $this->getCurrency();
42
+ return $currency->getCode();
43
+ }
44
+
45
+ /**
46
+ * @return double|false Manufacturer's suggested retail price.
47
+ */
48
+ public function getMsrp()
49
+ {
50
+ return $this->calculatePrice(
51
+ $this->getProductsForOtherPriceFields(),
52
+ 'getMsrpForProduct'
53
+ );
54
+ }
55
+
56
+ /**
57
+ * @return double|false The regular or "compare at" price, if known.
58
+ */
59
+ public function getRegularPrice()
60
+ {
61
+ $regular = $this->calculatePrice(
62
+ $this->getProductsForOtherPriceFields(),
63
+ 'getRegularPriceForProduct'
64
+ );
65
+
66
+ if ($regular === false) {
67
+ return false;
68
+ }
69
+
70
+ // ONly return regular price if it is greater than than retail
71
+ $retail = $this->getRetailPrice();
72
+ if ($retail === false) {
73
+ // No retail = can't figure out regular price
74
+ return false;
75
+ }
76
+
77
+ // Only return "regular price" if it 0.01 or greater than retail
78
+ $diff = $regular - $retail;
79
+ if ($diff > 0.01) {
80
+ return $regular;
81
+ }
82
+
83
+ return false;
84
+ }
85
+
86
+ /**
87
+ * @return double|false The current full retail price of the product(s).
88
+ */
89
+ public function getRetailPrice()
90
+ {
91
+ return $this->calculatePrice(
92
+ $this->getProductsForRetailPrice(),
93
+ 'getRetailPriceForProduct'
94
+ );
95
+ }
96
+
97
+ /**
98
+ * @internal Aggregates the value returned by $getter for all products considered for pricing.
99
+ * @param array $products
100
+ * @param string $getter
101
+ * @return double|false
102
+ */
103
+ protected function calculatePrice(array $products, $getter)
104
+ {
105
+ $result = false;
106
+
107
+ foreach($products as $product) {
108
+ $valueForProduct = $this->$getter($product);
109
+ if ($valueForProduct === false) {
110
+ // False for any 1 product = false for all.
111
+ return false;
112
+ }
113
+
114
+ $qty = $this->getEffectiveQtyForProduct($product);
115
+ $valueForProduct *= $qty;
116
+
117
+ if ($result === false) {
118
+ $result = $valueForProduct;
119
+ } else {
120
+ $result += $valueForProduct;
121
+ }
122
+ }
123
+
124
+ return $result === false ? $result : doubleval($result);
125
+ }
126
+
127
+ /**
128
+ * @param Mage_Catalog_Model_Product $product
129
+ * @return double|false Retailer's cost for the given product.
130
+ */
131
+ protected function getCostForProduct(Mage_Catalog_Model_Product $product)
132
+ {
133
+ $cost = $product->getCost();
134
+ return is_numeric($cost) ? doubleval($cost) : false;
135
+ }
136
+
137
+ /**
138
+ * @param Mage_Catalog_Model_Product $product
139
+ * @return integer The number of $product being bought.
140
+ */
141
+ protected function getEffectiveQtyForProduct(Mage_Catalog_Model_Product $product)
142
+ {
143
+ $isChild = !!$product->getParentProductId();
144
+ if (!$isChild) {
145
+ // Don't consider quantity for parent products
146
+ return 1;
147
+ }
148
+
149
+ // For child products (i.e., items in a bundle, use cart qty as multiplier to determine price
150
+
151
+ $qty = $product->getCartQty();
152
+
153
+ if ($qty > 0) {
154
+ return intval($qty);
155
+ }
156
+
157
+ return 1;
158
+ }
159
+
160
+ /**
161
+ * @param Mage_Catalog_Model_Product $product
162
+ * @return double|false Manufacturer's suggested retail price (if known).
163
+ */
164
+ protected function getMsrpForProduct(Mage_Catalog_Model_Product $product)
165
+ {
166
+ $msrp = $product->getMsrp();
167
+
168
+ if (is_numeric($msrp)) {
169
+ return doubleval($msrp);
170
+ }
171
+
172
+ return false;
173
+ }
174
+
175
+ /**
176
+ * @param Mage_Catalog_Model_Product $product
177
+ * @return double|false "Compare at" price for the product (if known).
178
+ */
179
+ protected function getRegularPriceForProduct(Mage_Catalog_Model_Product $product)
180
+ {
181
+ $candidates = array();
182
+
183
+ $nonSpecialPrice = $product->getPrice();
184
+ if ($nonSpecialPrice > 0) {
185
+ $candidates[] = doubleval($nonSpecialPrice);
186
+ }
187
+
188
+ // Let MSRP factor in
189
+ $msrp = $this->getMsrpForProduct($product);
190
+ if ($msrp > 0) {
191
+ $candidates[] = doubleval($msrp);
192
+ }
193
+
194
+ if (empty($candidates)) {
195
+ return false;
196
+ }
197
+
198
+ sort($candidates);
199
+ return $candidates[0];
200
+ }
201
+
202
+ /**
203
+ * @param Mage_Catalog_Model_Product $product
204
+ * @return double|false The current full retail price for the given product.
205
+ */
206
+ protected function getRetailPriceForProduct(Mage_Catalog_Model_Product $product)
207
+ {
208
+ $retail = $product->getFinalPrice();
209
+
210
+ if (is_numeric($retail)) {
211
+ return doubleval($retail);
212
+ }
213
+
214
+ return false;
215
+ }
216
+
217
+ /**
218
+ * @return array The set of products to use when summing up the final retail price.
219
+ */
220
+ protected function getProductsForRetailPrice()
221
+ {
222
+ // Any non-child product *should* have a good retail price attached to it.
223
+
224
+ $result = array();
225
+
226
+ foreach($this->_products as $product) {
227
+ $isChild = !!$product->getParentProductId();
228
+ if (!$isChild) {
229
+ $result[] = $product;
230
+ }
231
+ }
232
+
233
+ return $result;
234
+ }
235
+
236
+ /**
237
+ * @return array The set of products to use when summing up fields *other* than final retail price.
238
+ */
239
+ protected function getProductsForOtherPriceFields()
240
+ {
241
+ // *If* we have child products here, return *only* those.
242
+ // Otherwise return all products.
243
+
244
+ $haveChildProducts = false;
245
+ foreach($this->_products as $product) {
246
+ $isChild = !!$product->getParentProductId();
247
+ if ($isChild) {
248
+ $haveChildProducts = true;
249
+ break;
250
+ }
251
+ }
252
+
253
+ if (!$haveChildProducts) {
254
+ return $this->_products;
255
+ }
256
+
257
+ // Include only the child products.
258
+ $result = array();
259
+ foreach($this->_products as $product) {
260
+ $isChild = !!$product->getParentProductId();
261
+ if ($isChild) {
262
+ $result[] = $product;
263
+ }
264
+ }
265
+
266
+ return $result;
267
+ }
268
+
269
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Session.php ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Class responsible for tracking the user's currently applied deals.
5
+ */
6
+ class PriceWaiter_NYPWidget_Model_Session
7
+ extends Mage_Core_Model_Session_Abstract
8
+ {
9
+ private $_activeDeals = null;
10
+
11
+ private $_now = null;
12
+
13
+ public function __construct()
14
+ {
15
+ $this->init('pricewaiter');
16
+ }
17
+
18
+ /**
19
+ * @return String The unique PriceWaiter ID of the current buyer.s
20
+ */
21
+ public function getBuyerId()
22
+ {
23
+ return $this->getData('buyer_id');
24
+ }
25
+
26
+ /**
27
+ * Sets the current buyer id.
28
+ * @param $id
29
+ * @return PriceWaiter_NYPWidget_Model_Session $this
30
+ */
31
+ public function setBuyerId($id)
32
+ {
33
+ $this->setData('buyer_id', $id);
34
+ $this->_activeDeals = null;
35
+ return $this;
36
+ }
37
+
38
+ /**
39
+ * @return Integer UNIX timestamp representing "now".
40
+ */
41
+ public function getNow()
42
+ {
43
+ if ($this->_now === null) {
44
+ return time();
45
+ }
46
+
47
+ return $this->_now;
48
+ }
49
+
50
+ /**
51
+ * @internal For tests.
52
+ * @param Integer $now
53
+ */
54
+ public function setNow($now)
55
+ {
56
+ if (is_string($now)) {
57
+ $now = strtotime($now);
58
+ }
59
+
60
+ $this->_now = $now;
61
+
62
+ return $this;
63
+ }
64
+
65
+ /**
66
+ * @return Array All Deals for the buyer that are unrevoked and unexpired.
67
+ */
68
+ public function getActiveDeals()
69
+ {
70
+ if ($this->_activeDeals !== null) {
71
+ return $this->_activeDeals;
72
+ }
73
+
74
+ $buyerId = $this->getBuyerId();
75
+
76
+ if (!$buyerId) {
77
+ return array();
78
+ }
79
+
80
+ $collection = Mage::getModel('nypwidget/deal')
81
+ ->getCollection()
82
+ // Only get Deals for the current buyer...
83
+ ->addFieldToFilter('pricewaiter_buyer_id', $buyerId)
84
+
85
+ // ...that haven't been revoked...
86
+ ->addFieldToFilter('revoked', 0)
87
+
88
+ // ...and havent' already been used to check out...
89
+ ->addFieldToFilter('order_id', array('null' => true))
90
+
91
+ // ...and either don't have an expiry or have an expiry in the future
92
+ ->addFieldToFilter(
93
+ 'expires_at',
94
+ array(
95
+ array('gt' => date('Y-m-d H:i:s', $this->getNow())),
96
+ array('null' => true),
97
+ )
98
+ );
99
+
100
+ $this->_activeDeals = $collection->getItems();
101
+
102
+ return $this->_activeDeals;
103
+ }
104
+
105
+ /**
106
+ * @return $this
107
+ */
108
+ public function reset()
109
+ {
110
+ return $this->setBuyerId(null);
111
+ }
112
+ }
app/code/community/PriceWaiter/NYPWidget/Model/Total/Quote.php ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Total model used to apply PriceWaiter discounts at the quote level.
5
+ */
6
+ class PriceWaiter_NYPWidget_Model_Total_Quote
7
+ extends Mage_Sales_Model_Quote_Address_Total_Abstract
8
+ {
9
+ private $_deals = null;
10
+ private $_session = null;
11
+
12
+ /**
13
+ * @internal
14
+ */
15
+ private static $_dealsForTesting = null;
16
+
17
+ /**
18
+ * @internal Supports fetch() hack.
19
+ * @var array
20
+ */
21
+ private static $_fetchingAddresses = array();
22
+
23
+ /**
24
+ * Calculates PriceWaiter discounts and applies them to the quote and
25
+ * relevant quote items.
26
+ *
27
+ * @param Mage_Sales_Model_Quote_Address $address
28
+ * @return PriceWaiter_NYPWidget_Model_Total_Quote $this
29
+ */
30
+ public function collect(Mage_Sales_Model_Quote_Address $address)
31
+ {
32
+ parent::collect($address);
33
+
34
+ if (!$this->shouldCollectAddress($address)) {
35
+ return $this;
36
+ }
37
+
38
+ $deals = $this->getPriceWaiterDeals();
39
+
40
+ // In case of collision, favor more recent deals over less recent.
41
+ usort($deals, array(__CLASS__, 'sortDealsRecentFirst'));
42
+
43
+ $appliedDeals = array();
44
+ $appliedDealIds = array();
45
+
46
+ $discountedQuoteItems = array();
47
+ $pwDiscount = 0;
48
+
49
+ /** @var PriceWaiter_NYPWidget_Model_Deal $deal */
50
+ foreach ($deals as $deal) {
51
+ $applied = $this->collectDeal($deal, $address, $discountedQuoteItems);
52
+
53
+ if (!$applied) {
54
+ continue;
55
+ }
56
+
57
+ $pwDiscount += $applied;
58
+
59
+ $appliedDeals[] = $deal;
60
+ $appliedDealIds[] = $deal->getId();
61
+ }
62
+
63
+ // Note that we have used one or more deals for this quote.
64
+ // This connection will get picked up later when quote is
65
+ // converted into an order.
66
+ // TODO: Link Deal -> Order on order creation without requiring an
67
+ // intermediate link btw. Deal -> Quote.
68
+
69
+ $res = Mage::getResourceModel('nypwidget/deal_usage');
70
+ $res->recordDealUsageForQuote(
71
+ $address->getQuote(),
72
+ $appliedDeals
73
+ );
74
+
75
+ // Track deal usage and discount amounts. fetch(), below, will pick them up.
76
+ // Note that these fields are *not* perisisted directly to the DB.
77
+ $address->setPriceWaiterDiscount($pwDiscount);
78
+ $address->setPriceWaiterDealIds($appliedDealIds);
79
+
80
+ return $this;
81
+ }
82
+
83
+ /**
84
+ * Adds the "PriceWaiter Savings" totals row for the address.
85
+ *
86
+ * @param Mage_Sales_Model_Quote_Address $address
87
+ * @return PriceWaiter_NYPWidget_Model_Total_Quote $this
88
+ */
89
+ public function fetch(Mage_Sales_Model_Quote_Address $address)
90
+ {
91
+ $discount = $address->getPriceWaiterDiscount();
92
+ $deals = $address->getPriceWaiterDealIds();
93
+
94
+ if (!$deals || !$discount) {
95
+ return $this;
96
+ }
97
+
98
+ if ($this->fetchOverridingDiscount($address)) {
99
+ // We sneakily overrode the existing discount total
100
+ return $this;
101
+ }
102
+
103
+ // If Discount not loaded, then we can't replace it so we just add ours.
104
+ $address->addTotal($this->buildPriceWaiterTotal(-$discount));
105
+
106
+ return $this;
107
+ }
108
+
109
+ protected function buildPriceWaiterTotal($discount)
110
+ {
111
+ $helper = Mage::helper('nypwidget');
112
+
113
+ return array(
114
+ 'code' => 'pricewaiter',
115
+ 'title' => $helper->__('PriceWaiter Savings'),
116
+ 'value' => $discount,
117
+ );
118
+ }
119
+
120
+ protected function fetchOverridingDiscount(Mage_Sales_Model_Quote_Address $address)
121
+ {
122
+ $addrId = $address->getId() ? $address->getId() : spl_object_hash($address);
123
+ $alreadyFetching = !empty(self::$_fetchingAddresses[$addrId]);
124
+
125
+ if ($alreadyFetching) {
126
+ // Avoid stack overflow.
127
+ return true;
128
+ }
129
+
130
+ // Note that we are fetching for this address so that when we call
131
+ // $addr->getTotals() below we don't end up recursing infinitely.
132
+ self::$_fetchingAddresses[$addrId] = true;
133
+
134
+ try
135
+ {
136
+ $discountTotal = null;
137
+ foreach ($address->getTotals() as $total) {
138
+ if ($total->getCode() === 'discount') {
139
+ $discountTotal = $total;
140
+ break;
141
+ }
142
+ }
143
+
144
+ unset(self::$_fetchingAddresses[$addrId]);
145
+
146
+ if ($discountTotal) {
147
+ $this->hackilyOverwriteDiscountTotal($discountTotal);
148
+ return true;
149
+ }
150
+
151
+ return false; // "We did not overwrite discount"
152
+ }
153
+ catch (Exception $ex)
154
+ {
155
+ unset(self::$_fetchingAddresses[$addrId]);
156
+ throw $ex;
157
+ }
158
+ }
159
+
160
+ protected function hackilyOverwriteDiscountTotal(Mage_Sales_Model_Quote_Address_Total $total)
161
+ {
162
+ $pwTotal = $this->buildPriceWaiterTotal($total->getValue());
163
+ $total->setCode($pwTotal['code']);
164
+ $total->setTitle($pwTotal['title']);
165
+ }
166
+
167
+ /**
168
+ * @return array Deal models to be applied for discounting.
169
+ */
170
+ public function getPriceWaiterDeals()
171
+ {
172
+ // HACK: Allow injecting deals for tests, since we don't control
173
+ // instantiation of this class during Quote::collectTotals().
174
+ if (self::$_dealsForTesting !== null) {
175
+ return self::$_dealsForTesting;
176
+ }
177
+
178
+ if ($this->_deals === null) {
179
+ $this->initDealsFromSession();
180
+ }
181
+
182
+ return $this->_deals;
183
+ }
184
+
185
+ /**
186
+ * @internal Setter for deals array.
187
+ * @param Array $deals Deals
188
+ * @return PriceWaiter_NYPWidget_Model_Total_Quote $this
189
+ */
190
+ public function setPriceWaiterDeals(Array $deals)
191
+ {
192
+ $this->_deals = $deals;
193
+ return $this;
194
+ }
195
+
196
+ /**
197
+ * @return PriceWaiter_NYPWidget_Model_Session
198
+ */
199
+ public function getSession()
200
+ {
201
+ return $this->_session ?
202
+ $this->_session :
203
+ Mage::getSingleton('nypwidget/session');
204
+ }
205
+
206
+ /**
207
+ * Sets the session instance to use (instead of the default one).
208
+ * @param PriceWaiter_NYPWidget_Model_Session $session
209
+ */
210
+ public function setSession(PriceWaiter_NYPWidget_Model_Session $session)
211
+ {
212
+ $this->_session = $session;
213
+ return $this;
214
+ }
215
+
216
+ /**
217
+ * @internal Sets the deals to be returnd by getPriceWaiterDeals().
218
+ * @param array $deals
219
+ */
220
+ public static function hackilySetDealsForTesting($deals)
221
+ {
222
+ self::$_dealsForTesting = $deals;
223
+ }
224
+
225
+ /**
226
+ * @internal
227
+ * @param PriceWaiter_NYPWidget_Model_Deal $a
228
+ * @param PriceWaiter_NYPWidget_Model_Deal $b
229
+ * @return Integer
230
+ */
231
+ public static function sortDealsRecentFirst(
232
+ PriceWaiter_NYPWidget_Model_Deal $a,
233
+ PriceWaiter_NYPWidget_Model_Deal $b
234
+ )
235
+ {
236
+ $aCreated = @strtotime($a->getCreatedAt());
237
+ $bCreated = @strtotime($b->getCreatedAt());
238
+
239
+ // If a is more recent than b, sort it before
240
+ return $bCreated - $aCreated;
241
+ }
242
+
243
+ /**
244
+ * Attempts to apply a Deal to the given Quote_Address.
245
+ * @param PriceWaiter_NYPWidget_Model_Deal $deal
246
+ * @param Mage_Sales_Model_Quote_Address $address
247
+ * @param array $discountedQuoteItems Tracks quote items that receive discounts.
248
+ * @return array|false
249
+ */
250
+ protected function collectDeal(
251
+ PriceWaiter_NYPWidget_Model_Deal $deal,
252
+ Mage_Sales_Model_Quote_Address $address,
253
+ array &$discountedQuoteItems
254
+ )
255
+ {
256
+ $offerItems = $deal->getOfferItems();
257
+
258
+ $quote = $address->getQuote();
259
+ $quoteItems = $address->getAllItems();
260
+
261
+ $quoteItemsForOfferItems = array();
262
+
263
+ try
264
+ {
265
+ // Check that we can apply *all* offer items.
266
+ foreach($offerItems as $offerItem) {
267
+ $quoteItem = $offerItem->findQuoteItem($quoteItems);
268
+
269
+ if (!$quoteItem) {
270
+ // No candidate quote item found -- cannot apply this deal.
271
+ return false;
272
+ }
273
+
274
+ if (in_array($quoteItem, $discountedQuoteItems)) {
275
+ // This quote item has already had a PriceWaiter deal applied.
276
+ // PW Deals *should not* stack on top of each other.
277
+ return false;
278
+ }
279
+
280
+ $quoteItemsForOfferItems[] = $quoteItem;
281
+ }
282
+ }
283
+ catch (Exception $ex)
284
+ {
285
+ // Prevent malformed / invalid deals from killing the cart.
286
+ Mage::logException($ex);
287
+ return false;
288
+ }
289
+
290
+ // TODO: Existing discount conflict resolution
291
+
292
+ // If we've gotten this far, it means all offer items are good to apply.
293
+
294
+ $pwDiscount = 0;
295
+
296
+ foreach($offerItems as $index => $offerItem) {
297
+
298
+ $quoteItem = $quoteItemsForOfferItems[$index];
299
+
300
+ // Things we need to do here:
301
+ // 1. Get the *price* of the product in question.
302
+ // 2. Calculate how much of a discount to apply
303
+ // 3. Apply the discount to the appropriate quote item
304
+ // 4. Apply the discount to the address.
305
+
306
+ $discounter = $this->createDiscountCalculator(
307
+ $quote,
308
+ $quoteItem,
309
+ $offerItem
310
+ );
311
+
312
+ list($discount, $baseDiscount, $originalDiscount, $baseOriginalDiscount) = array(
313
+ $discounter->getDiscount(),
314
+ $discounter->getBaseDiscount(),
315
+ $discounter->getOriginalDiscount(),
316
+ $discounter->getBaseOriginalDiscount(),
317
+ );
318
+
319
+ if ($discount <= 0) {
320
+ continue;
321
+ }
322
+
323
+ // Note that we're altering this quote item so we don't
324
+ // try to alter it again.
325
+ $discountedQuoteItems[] = $quoteItem;
326
+
327
+ $quoteItem->setDiscountAmount($quoteItem->getDiscountAmount() + $discount);
328
+ $quoteItem->setBaseDiscountAmount($quoteItem->getBaseDiscountAmount() + $baseDiscount);
329
+
330
+ // These fields are not persisted, but may be used in tax calculation
331
+ $quoteItem->setOriginalDiscountAmount($quoteItem->getOriginalDiscount + $originalDiscount);
332
+ $quoteItem->setBaseOriginalDiscountAmount($quoteItem->getBaseOriginalDiscountAmount() + $baseOriginalDiscount);
333
+
334
+ $this->_addAmount(-$discount);
335
+ $this->_addBaseAmount(-$baseDiscount);
336
+
337
+ // NOTE: Setting Discount Amount on the *address* here will make the
338
+ // totals row for the built-in discounter show up.
339
+ // We should probably be storing our discount in different places.
340
+
341
+ $address->setDiscountAmount(
342
+ $address->getDiscountAmount() - $discount
343
+ );
344
+
345
+ $address->setBaseDiscountAmount(
346
+ $address->getBaseDiscountAmount() - $baseDiscount
347
+ );
348
+
349
+
350
+ $pwDiscount += $discount;
351
+ }
352
+
353
+ return $pwDiscount;
354
+ }
355
+
356
+ /**
357
+ * Configures a discounter to actually calculate how big a discount to apply.
358
+ */
359
+ protected function createDiscountCalculator(
360
+ Mage_Sales_Model_Quote $quote,
361
+ Mage_Sales_Model_Quote_Item $quoteItem,
362
+ PriceWaiter_NYPWidget_Model_Offer_Item $offerItem
363
+ )
364
+ {
365
+
366
+ return Mage::getModel('nypwidget/discounter')
367
+ ->setProductPrice($this->getItemPrice($quoteItem))
368
+ ->setProductOriginalPrice($this->getItemOriginalPrice($quoteItem))
369
+
370
+ ->setQuoteBaseCurrency($this->getCurrencyByCode($quote->getBaseCurrency()))
371
+ ->setQuoteCurrency($this->getCurrencyByCode($quote->getCurrency()))
372
+ ->setQuoteItemQty($quoteItem->getQty())
373
+
374
+ ->setOfferCurrency($this->getCurrencyByCode($offerItem->getCurrencyCode()))
375
+ ->setOfferAmountPerItem($offerItem->getAmountPerItem())
376
+ ->setOfferMinQty($offerItem->getMinimumQuantity())
377
+ ->setOfferMaxQty($offerItem->getMaximumQuantity())
378
+ ;
379
+ }
380
+
381
+ protected function getCurrencyByCode($code)
382
+ {
383
+ return Mage::getModel('directory/currency')->load($code);
384
+ }
385
+
386
+ /**
387
+ * @internal Cribbed from SalesRule/Model/Validator.php
388
+ */
389
+ protected function getItemPrice(Mage_Sales_Model_Quote_Item_Abstract $item)
390
+ {
391
+ $price = $item->getDiscountCalculationPrice();
392
+ $calcPrice = $item->getCalculationPrice();
393
+ return ($price !== null) ? $price : $calcPrice;
394
+ }
395
+
396
+ /**
397
+ * @internal Cribbed from SalesRule/Model/Validator.php
398
+ */
399
+ protected function getItemOriginalPrice(Mage_Sales_Model_Quote_Item_Abstract $item)
400
+ {
401
+ return Mage::helper('tax')->getPrice($item, $item->getOriginalPrice(), true);
402
+ }
403
+
404
+ /**
405
+ * Examines the current user's session and loads their current PriceWaiter deals.
406
+ * @return void
407
+ */
408
+ protected function initDealsFromSession()
409
+ {
410
+ $session = $this->getSession();
411
+ $this->setPriceWaiterDeals($session->getActiveDeals());
412
+ }
413
+
414
+ /**
415
+ * Hook to prevent collect() and fetch() on a given address.
416
+ * @param Mage_Sales_Model_Quote_Address $addr
417
+ * @return Boolean
418
+ */
419
+ protected function shouldCollectAddress(Mage_Sales_Model_Quote_Address $addr)
420
+ {
421
+ // HACK: We only support collecting for items on the shipping address for now.
422
+ // To properly support all address types we need a minor db migration +
423
+ // reworking of recordDealUsageForQuote() to accept address id as well as
424
+ // quote id.
425
+ if ($addr->getAddressType() !== Mage_Sales_Model_Quote_Address::TYPE_SHIPPING) {
426
+ return false;
427
+ }
428
+
429
+ // Things that stop us from performing collection:
430
+ // 1. Quote has salesrules applied
431
+ // 2. (Probably redundant) quote has coupon_code
432
+
433
+ // TODO: More nuance here.
434
+
435
+ $quote = $addr->getQuote();
436
+
437
+ $hasSalesRules = !empty($quote->getAppliedRuleIds());
438
+ if ($hasSalesRules) {
439
+ return false;
440
+ }
441
+
442
+ // This is probably redundant, a coupon code *should* result in an
443
+ // applied sales rule id (above), but just in case...
444
+ $hasCouponCode = !empty($quote->getCouponCode());
445
+ if ($hasCouponCode) {
446
+ return false;
447
+ }
448
+
449
+ return true;
450
+ }
451
+
452
+ }
app/code/community/PriceWaiter/NYPWidget/controllers/Adminhtml/PricewaiterController.php CHANGED
@@ -91,18 +91,4 @@ class PriceWaiter_NYPWidget_Adminhtml_PriceWaiterController extends Mage_Adminht
91
  'secret' => $secret
92
  )));
93
  }
94
-
95
- /**
96
- * @internal
97
- */
98
- protected function _isAllowed()
99
- {
100
- // We have to override this method for Magento Technical Validation.
101
- // The idea is that versions of Magento < 1.9.2 (or missing
102
- // SUPEE-6285 patch) just return `true` here by default, which can
103
- // inadvertently enable access to admin users w/o permissions.
104
- // We don't really use ACLs, so this shouldn't matter much, but
105
- // here we fill in what 1.9.2 does:
106
- return Mage::getSingleton('admin/session')->isAllowed('admin');
107
- }
108
  }
91
  'secret' => $secret
92
  )));
93
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  }
app/code/community/PriceWaiter/NYPWidget/controllers/CallbackController.php CHANGED
@@ -29,7 +29,7 @@ class PriceWaiter_NYPWidget_CallbackController extends Mage_Core_Controller_Fron
29
  }
30
 
31
  // Add debugging headers
32
- Mage::helper('nypwidget')->setHeaders($httpResponse);
33
  $pricewaiterId = '';
34
 
35
  try
29
  }
30
 
31
  // Add debugging headers
32
+ Mage::helper('nypwidget/about')->setResponseHeaders($httpResponse);
33
  $pricewaiterId = '';
34
 
35
  try
app/code/community/PriceWaiter/NYPWidget/controllers/CheckoutController.php ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Controller that serves the checkout_url endpoint used for deals.
5
+ */
6
+ class PriceWaiter_NYPWidget_CheckoutController
7
+ extends Mage_Core_Controller_Front_Action
8
+ {
9
+ /**
10
+ * Header used to provide feedback about errors.
11
+ * This is non-spec behavior added as a convenience.
12
+ */
13
+ const ERROR_HEADER = 'X-PriceWaiter-Error';
14
+
15
+ /**
16
+ * @internal
17
+ */
18
+ public function indexAction()
19
+ {
20
+ $httpRequest = $this->getRequest();
21
+ $httpResponse = $this->getResponse();
22
+
23
+ try
24
+ {
25
+
26
+ $id = $httpRequest->getQuery(
27
+ PriceWaiter_NYPWidget_Model_Deal::CHECKOUT_URL_DEAL_ID_ARG,
28
+ null
29
+ );
30
+
31
+ $deal = $this->getDealById($id);
32
+
33
+ if (!$deal) {
34
+ $this->redirectToHomepage('deal_not_found');
35
+ return;
36
+ }
37
+
38
+ $this->prepareCart($deal);
39
+
40
+ // Attempt to include some debugging info with redirect
41
+ $errorCode = null;
42
+
43
+ if ($deal->isRevoked()) {
44
+ $errorCode = 'deal_revoked';
45
+ } else if ($deal->isExpired()) {
46
+ $errorCode = 'deal_expired';
47
+ }
48
+
49
+ $this->redirectToCart($errorCode);
50
+ }
51
+ catch (PriceWaiter_NYPWidget_Exception_Product_OutOfStock $ex)
52
+ {
53
+ // The product is not currently in stock, so our add-to-cart
54
+ // failed. Forward the user to the cart page and show an
55
+ // error message (this matches built-in A2C error behavior)
56
+ $this->redirectToProductPage(
57
+ $deal,
58
+ $ex->errorCode,
59
+ $ex->getMessage()
60
+ );
61
+ }
62
+ catch (PriceWaiter_NYPWidget_Exception_Abstract $ex)
63
+ {
64
+ Mage::logException($ex);
65
+ $this->redirectToHomepage($ex->errorCode);
66
+ }
67
+ catch (Exception $ex)
68
+ {
69
+ Mage::logException($ex);
70
+ $this->redirectToHomepage();
71
+ }
72
+ }
73
+
74
+ /**
75
+ * @internal
76
+ */
77
+ protected function addMessageToSession($message)
78
+ {
79
+ // This is adapted from CartController's A2C handling.
80
+ // The idea is that if there was an error adding to cart, we should
81
+ // report that error to the user in some way.
82
+ $session = Mage::getSingleton('checkout/session');
83
+ $shouldUseNotice = $session->getUseNotice(true);
84
+
85
+ if ($shouldUseNotice) {
86
+ $message = Mage::helper('core')->escapeHtml($message);
87
+ $session->addNotice($message);
88
+ } else {
89
+ $messages = array_unique(explode("\n", $message));
90
+ foreach ($messages as $message) {
91
+ $message = Mage::helper('core')->escapeHtml($message);
92
+ $session->addError($message);
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * @param String $id
99
+ * @return PriceWaiter_NYPWidget_Model_Deal|false
100
+ */
101
+ protected function getDealById($id)
102
+ {
103
+ if (!$id) {
104
+ return false;
105
+ }
106
+
107
+ $deal = Mage::getModel('nypwidget/deal');
108
+ $deal->load($id);
109
+
110
+ return $deal->getId() ? $deal : false;
111
+ }
112
+
113
+ /**
114
+ * Ensures that the buyer's cart contains the contents of the given deal.
115
+ * @param PriceWaiter_NYPWidget_Model_Deal $deal
116
+ */
117
+ protected function prepareCart(PriceWaiter_NYPWidget_Model_Deal $deal)
118
+ {
119
+ $cart = Mage::getSingleton('checkout/cart');
120
+ $quote = $cart->getQuote();
121
+ $deal->ensurePresentInQuote($quote);
122
+ $cart->save();
123
+
124
+ // Track the current PW buyer ID so we can automatically discover
125
+ // other deals the buyer has made.
126
+ Mage::getSingleton('nypwidget/session')
127
+ ->setBuyerId($deal->getPricewaiterBuyerId());
128
+ }
129
+
130
+ /**
131
+ * Redirects the buyer back to the cart page.
132
+ * @param string $errorCode
133
+ */
134
+ protected function redirectToCart($errorCode = null, $errorMessage = null)
135
+ {
136
+ $url = Mage::getUrl('checkout/cart');
137
+ $this->_doRedirectWithError($url, $errorCode, $errorMessage);
138
+ }
139
+
140
+ /**
141
+ * Redirects the buyer back to the store's homepage.
142
+ * Used when deal is invalid in some way.
143
+ */
144
+ protected function redirectToHomepage($errorCode = null)
145
+ {
146
+ $url = Mage::getUrl('/');
147
+ $this->_doRedirectWithError($url, $errorCode);
148
+ }
149
+
150
+ /**
151
+ * Sends the user to the product page, optionally setting an error
152
+ * code header and displaying an error message.
153
+ *
154
+ * If something goes wrong, the user is redirected to the homepage.
155
+ *
156
+ * @param PriceWaiter_NYPWidget_Model_Deal $deal
157
+ * @param string $errorCode
158
+ * @param string $errorMessage
159
+ */
160
+ protected function redirectToProductPage(
161
+ PriceWaiter_NYPWidget_Model_Deal $deal,
162
+ $errorCode = null,
163
+ $errorMessage = null
164
+ )
165
+ {
166
+ $offerItems = $deal->getOfferItems();
167
+ if (count($offerItems) > 0) {
168
+ try
169
+ {
170
+ $url = $offerItems[0]->getMagentoProductUrl();
171
+ return $this->_doRedirectWithError($url, $errorCode, $errorMessage);
172
+ }
173
+ catch (Exception $ex)
174
+ {
175
+ // A malformed deal could result in getMagentoProductUrl() throwing
176
+ Mage::logException($ex);
177
+ }
178
+ }
179
+
180
+ // Fall back to homepage when something horrible happens
181
+ return $this->redirectToHomepage($errorCode);
182
+ }
183
+
184
+ /**
185
+ * @internal
186
+ * 302 Redirects the user, optionally setting an error code header
187
+ * and session-based error message.
188
+ */
189
+ private function _doRedirectWithError($url, $errorCode = null, $errorMessage = null)
190
+ {
191
+ $httpResponse = $this->getResponse();
192
+
193
+ if ($errorCode !== null) {
194
+ $httpResponse->setHeader(self::ERROR_HEADER, $errorCode, true);
195
+ }
196
+
197
+ $httpResponse->setRedirect($url);
198
+
199
+ if ($errorMessage !== null) {
200
+ $this->addMessageToSession($errorMessage);
201
+ }
202
+ }
203
+
204
+ }
app/code/community/PriceWaiter/NYPWidget/controllers/CreatedealController.php ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ * Copyright 2013-2016 Price Waiter, LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ /**
21
+ * Controller that handles /pricewaiter/createdeal
22
+ */
23
+ class PriceWaiter_NYPWidget_CreatedealController extends PriceWaiter_NYPWidget_Controller_Endpoint
24
+ {
25
+ /**
26
+ * Versions of request data this controller supports.
27
+ * @var Array
28
+ */
29
+ protected $supportedVersions = [
30
+ '2016-03-01',
31
+ ];
32
+
33
+ public function processRequest(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request)
34
+ {
35
+ $deal = Mage::getModel('nypwidget/deal');
36
+
37
+ $deal->processCreateRequest($request);
38
+
39
+ $response = new PriceWaiter_NYPWidget_Controller_Endpoint_Response(200, array(
40
+ 'checkout_url' => $deal->getCheckoutUrl(),
41
+ ));
42
+
43
+ return $response;
44
+ }
45
+
46
+ }
app/code/community/PriceWaiter/NYPWidget/controllers/DebugController.php ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ * Copyright 2013-2016 Price Waiter, LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ class PriceWaiter_NYPWidget_DebugController extends PriceWaiter_NYPWidget_Controller_Endpoint
21
+ {
22
+ /**
23
+ * @var Array
24
+ */
25
+ protected $supportedVersions = [
26
+ '2016-03-01',
27
+ ];
28
+
29
+ public function processRequest(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request)
30
+ {
31
+ $resp = array(
32
+ 'store' => $this->summarizeStore(),
33
+ 'total_models' => $this->summarizeTotalModels(),
34
+ );
35
+
36
+ return new PriceWaiter_NYPWidget_Controller_Endpoint_Response(200, $resp);
37
+ }
38
+
39
+ /**
40
+ * Returns a summary of the store config.
41
+ * @return Array
42
+ */
43
+ protected function summarizeStore()
44
+ {
45
+ $store = Mage::app()->getStore();
46
+ $helper = Mage::helper('nypwidget');
47
+
48
+ return array(
49
+ 'name' => $store->getName(),
50
+ 'code' => $store->getCode(),
51
+ 'pricewaiter_api_key' => $helper->getPriceWaiterApiKey($store),
52
+ 'pricewaiter_secret_set' => !!trim($helper->getSecret($store)),
53
+ );
54
+ }
55
+
56
+ /**
57
+ * Returns a summary of the available total models and their configuration.
58
+ * @return Array
59
+ */
60
+ protected function summarizeTotalModels()
61
+ {
62
+ $collector = Mage::getSingleton(
63
+ 'sales/quote_address_total_collector',
64
+ array('store' => Mage::app()->getStore())
65
+ );
66
+
67
+ $result = array();
68
+ foreach($collector->getCollectors() as $code => $collector) {
69
+ $result[] = array(
70
+ 'code' => $code,
71
+ 'class' => get_class($collector),
72
+ );
73
+ }
74
+
75
+ return $result;
76
+ }
77
+ }
app/code/community/PriceWaiter/NYPWidget/controllers/ListordersController.php ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Controller that handles /pricewaiter/listorders
4
+ */
5
+ class PriceWaiter_NYPWidget_ListordersController extends PriceWaiter_NYPWidget_Controller_Endpoint
6
+ {
7
+ /**
8
+ * Versions of request data this controller supports.
9
+ * @var Array
10
+ */
11
+ protected $supportedVersions = [
12
+ '2016-03-01',
13
+ ];
14
+
15
+ public function processRequest(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request)
16
+ {
17
+ $body = $request->getBody();
18
+ $res = Mage::getResourceModel('nypwidget/deal_usage');
19
+
20
+ // Resolve deal ids into a set of order -> deal id links
21
+ $usage = $res->getOrdersAndDealUsageForDealIds($body->pricewaiter_deals);
22
+
23
+ // Format and return the resulting array
24
+ $orders = $this->formatUsage($usage);
25
+
26
+ $response = new PriceWaiter_NYPWidget_Controller_Endpoint_Response(200, $orders);
27
+
28
+ return $response;
29
+ }
30
+
31
+ /**
32
+ * @internal
33
+ * @param array $usage
34
+ * @return array
35
+ */
36
+ public function formatUsage(array $usage)
37
+ {
38
+ $result = array();
39
+
40
+ foreach($usage as $u) {
41
+ $order = $u['order'];
42
+ $dealIds = $u['dealIds'];
43
+ $formatted = $this->formatOrder($order, $dealIds);
44
+ if ($formatted) {
45
+ $result[] = $formatted;
46
+ }
47
+ }
48
+
49
+ return $result;
50
+ }
51
+
52
+ /**
53
+ * Attempts to format
54
+ * @param Mage_Sales_Model_Order $order
55
+ * @param array $dealIds
56
+ * @return Object|false
57
+ */
58
+ public function formatOrder(Mage_Sales_Model_Order $order, array $dealIds)
59
+ {
60
+ $state = $this->translateMagentoOrderState($order->getState());
61
+ if (!$state) {
62
+ return false;
63
+ }
64
+
65
+ return (object)array(
66
+ 'id' => strval($order->getIncrementId()),
67
+ 'state' => $state,
68
+ 'currency' => $order->getOrderCurrency()->getCode(),
69
+ 'subtotal' => (object)array(
70
+ 'value' => strval(
71
+ // "subtotal" here means "order total minus shipping and tax"
72
+ $order->getGrandTotal() -
73
+ $order->getShippingAmount() -
74
+ $order->getTaxAmount()
75
+ ),
76
+ ),
77
+ 'pricewaiter_deals' => $dealIds,
78
+ );
79
+ }
80
+
81
+ /**
82
+ * @internal Translates a Magento order state into one of the states
83
+ * PriceWaiter uses.
84
+ * @param string $state
85
+ * @return string|false A PW state, or false if no translation is possible.
86
+ */
87
+ public static function translateMagentoOrderState($state)
88
+ {
89
+ $state = strtolower(strval($state));
90
+
91
+ switch($state) {
92
+ case Mage_Sales_Model_Order::STATE_CANCELED:
93
+ return false;
94
+
95
+ case Mage_Sales_Model_Order::STATE_PAYMENT_REVIEW:
96
+ case Mage_Sales_Model_Order::STATE_PENDING_PAYMENT:
97
+ return 'pending';
98
+
99
+ default:
100
+ return 'paid';
101
+ }
102
+ }
103
+ }
app/code/community/PriceWaiter/NYPWidget/controllers/PingController.php ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /**
4
+ * Controller that handles /pricewaiter/ping
5
+ */
6
+ class PriceWaiter_NYPWidget_PingController extends PriceWaiter_NYPWidget_Controller_Endpoint
7
+ {
8
+ /**
9
+ * Versions of request data this controller supports.
10
+ * @var Array
11
+ */
12
+ protected $supportedVersions = [
13
+ '2016-03-01',
14
+ ];
15
+
16
+ public function processRequest(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request)
17
+ {
18
+ $response = new PriceWaiter_NYPWidget_Controller_Endpoint_Response(200, $request->getBody());
19
+ return $response;
20
+ }
21
+
22
+ }
app/code/community/PriceWaiter/NYPWidget/controllers/ProductinfoController.php CHANGED
@@ -1,61 +1,161 @@
1
  <?php
2
 
3
- class PriceWaiter_NYPWidget_ProductinfoController extends Mage_Core_Controller_Front_Action
 
4
  {
5
  public function indexAction()
6
  {
7
- // Ensure that we have received POST data
8
- $requestBody = Mage::app()->getRequest()->getRawBody();
9
- $postFields = Mage::app()->getRequest()->getPost();
10
- Mage::helper('nypwidget')->setHeaders();
11
 
12
- $productHelper = Mage::helper('nypwidget/product');
13
 
14
- if (count($postFields) == 0) {
15
- $this->norouteAction();
16
- return;
17
- }
18
 
19
  // Validate the request
20
  // - return 400 if signature cannot be verified
21
- $signature = Mage::app()->getRequest()->getHeader('X-PriceWaiter-Signature');
22
- if (Mage::helper('nypwidget')->isPriceWaiterRequestValid($signature, $requestBody) == false) {
23
- Mage::app()->getResponse()->setHeader('HTTP/1.0 400 Bad Request Error', 400, true);
24
  return false;
25
  }
26
 
27
- // Process the request
28
- // - return 404 if the product does not exist (or PriceWaiter is not enabled)
29
- $productConfiguration = array();
30
- parse_str(urldecode($postFields['_magento_product_configuration']), $productConfiguration);
 
 
 
 
 
 
 
 
 
31
 
32
- // always lookup the product with a low quantity
33
- // the below code will fail if the product is out of stock
34
- if ($productConfiguration && isset($productConfiguration['qty'])) {
35
- $productConfiguration['qty'] = 1;
36
  }
 
 
 
37
 
38
- // Create a cart and add the product to it
39
- // This is necessary to make Magento calculate the cost of the item in the correct context.
40
- try {
41
- $productInformation = $productHelper->lookupData($productConfiguration);
42
 
43
- if ($productInformation) {
44
- // Sign response and send.
45
- $json = json_encode($productInformation);
46
- $signature = Mage::helper('nypwidget')->getResponseSignature($json);
 
 
 
 
 
 
 
 
 
 
 
47
 
48
- Mage::app()->getResponse()->setHeader('X-PriceWaiter-Signature', $signature);
49
- Mage::app()->getResponse()->setBody($json);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  } else {
51
- Mage::app()->getResponse()->setHeader('HTTP/1.0 404 Not Found', 404, true);
52
- return;
53
  }
54
- } catch (Exception $e) {
55
- Mage::log("Unable to fulfill PriceWaiter Product Information request for product ID: " . $productConfiguration['product']);
56
- Mage::log($e->getMessage());
57
- Mage::app()->getResponse()->setHeader('HTTP/1.0 404 Not Found', 404, true);
58
- return;
59
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
  }
1
  <?php
2
 
3
+ class PriceWaiter_NYPWidget_ProductinfoController
4
+ extends Mage_Core_Controller_Front_Action
5
  {
6
  public function indexAction()
7
  {
8
+ $httpRequest = $this->getRequest();
9
+ $httpResponse = $this->getResponse();
 
 
10
 
11
+ Mage::helper('nypwidget/about')->setResponseHeaders($httpResponse);
12
 
13
+ // Ensure that we have received POST data
14
+ $requestBody = $httpRequest->getRawBody();
15
+ $postFields = $httpRequest->getPost();
 
16
 
17
  // Validate the request
18
  // - return 400 if signature cannot be verified
19
+ $signature = $httpRequest->getHeader('X-PriceWaiter-Signature');
20
+ if (!$this->isPriceWaiterRequestValid($signature, $requestBody)) {
21
+ $httpResponse->setHttpResponseCode(400);
22
  return false;
23
  }
24
 
25
+ try
26
+ {
27
+ $store = Mage::app()->getStore();
28
+
29
+ // Turn an array of POST data into a thing that can actually
30
+ // tell us something about the product(s).
31
+ $offerItem = $this->getOfferItem($httpRequest->getPost(), $store);
32
+
33
+ // Format the result we're going to return.
34
+ $result = $this->buildResponse($offerItem, $store);
35
+
36
+ // And finally, return it.
37
+ $json = json_encode($result);
38
 
39
+ $httpResponse->setHeader('X-PriceWaiter-Signature', $this->getResponseSignature($json));
40
+ $httpResponse->setHeader('Content-Type', 'application/json');
41
+ $httpResponse->setBody($json);
 
42
  }
43
+ catch (Exception $ex)
44
+ {
45
+ Mage::logException($ex);
46
 
47
+ $httpResponse->setHttpResponseCode(404);
 
 
 
48
 
49
+ // Extra: Include an error code if we have one.
50
+ if ($ex instanceof PriceWaiter_NYPWidget_Exception_Abstract) {
51
+ $httpResponse->setHeader('X-PriceWaiter-Error', $ex->errorCode);
52
+ }
53
+ }
54
+ }
55
+
56
+ public function buildResponse(
57
+ PriceWaiter_NYPWidget_Model_Offer_Item $item,
58
+ Mage_Core_Model_Store $store
59
+ )
60
+ {
61
+ $result = array(
62
+ 'allow_pricewaiter' => true, // See pricewaiter/magento-dev#115
63
+ );
64
 
65
+ // 1. Add pricing information
66
+ $pricing = $item->getPricing();
67
+
68
+ $retail = $pricing->getRetailPrice();
69
+ if ($retail !== false) {
70
+ $result['retail_price'] = strval($retail);
71
+ $result['retail_price_currency'] = $pricing->getCurrencyCode();
72
+ }
73
+
74
+ $cost = $pricing->getCost();
75
+ if ($cost !== false) {
76
+ $result['cost'] = strval($cost);
77
+ $result['cost_currency'] = $pricing->getCurrencyCode();
78
+ }
79
+
80
+ $regular = $pricing->getRegularPrice();
81
+ if ($regular !== false) {
82
+ $result['regular_price'] = strval($regular);
83
+ $result['regular_price_currency'] = $pricing->getCurrencyCode();
84
+ }
85
+
86
+ // 2. Add inventory
87
+ $inventory = $item->getInventory();
88
+ $stock = $inventory->getStock();
89
+ if ($stock !== false) {
90
+ $result['inventory'] = $stock;
91
+ $result['can_backorder'] = $inventory->canBackorder();
92
+ }
93
+
94
+ return $result;
95
+ }
96
+
97
+ /**
98
+ * @return PriceWaiter_NYPWidget_Model_Offer_Item
99
+ */
100
+ public function getOfferItem(array $post, Mage_Core_Model_Store $store)
101
+ {
102
+ $data = array(
103
+ 'product' => array(),
104
+ 'metadata' => array(),
105
+ );
106
+
107
+ // Per spec, $post will contain product_sku, and any other field
108
+ // should be interpreted as metadata.
109
+ foreach($post as $key => $value) {
110
+ if ($key === 'product_sku') {
111
+ $data['product']['sku'] = $value;
112
  } else {
113
+ $data['metadata'][$key] = $value;
 
114
  }
 
 
 
 
 
115
  }
116
+
117
+ return Mage::getModel('nypwidget/offer_item', $data, $store);
118
+ }
119
+
120
+ /**
121
+ * Returns a signature that can be added to the head of a PriceWaiter API response.
122
+ * @param {String} $responseBody The full body of the request to sign.
123
+ * @return {String} Signature that should be set as the X-PriceWaiter-Signature header.
124
+ */
125
+ public function getResponseSignature($responseBody)
126
+ {
127
+ $secret = Mage::helper('nypwidget')->getSecret();
128
+ $signature = 'sha256=' . hash_hmac('sha256', $responseBody, $secret, false);
129
+ return $signature;
130
+ }
131
+
132
+ /**
133
+ * Validates that the current request came from PriceWaiter.
134
+ * @param {String} $signatureHeader Full value of the X-PriceWaiter-Signature header.
135
+ * @param {String} $requestBody Complete body of incoming request.
136
+ * @return {Boolean} Wehther the request actually came from PriceWaiter.
137
+ */
138
+ public function isPriceWaiterRequestValid($signatureHeader = null, $requestBody = null)
139
+ {
140
+ if ($signatureHeader === null || $requestBody === null) {
141
+ return false;
142
+ }
143
+
144
+ $secret = Mage::helper('nypwidget')->getSecret();
145
+
146
+ if (trim($secret) === '') {
147
+ // Don't allow a blank secret to validate.
148
+ return false;
149
+ }
150
+
151
+ $detected = 'sha256=' . hash_hmac('sha256', $requestBody, $secret, false);
152
+
153
+ if (function_exists('hash_equals')) {
154
+ // Favor PHP's secure hash comparison function in 5.6 and up.
155
+ // For a robust drop-in compatibility shim, see: https://github.com/indigophp/hash-compat
156
+ return hash_equals($detected, $signatureHeader);
157
+ }
158
+
159
+ return $detected === $signatureHeader;
160
  }
161
  }
app/code/community/PriceWaiter/NYPWidget/controllers/RevokedealController.php ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ /*
4
+ * Copyright 2013-2016 Price Waiter, LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ /**
21
+ * Controller that handles /pricewaiter/revokedeal
22
+ */
23
+ class PriceWaiter_NYPWidget_RevokedealController extends PriceWaiter_NYPWidget_Controller_Endpoint
24
+ {
25
+ /**
26
+ * Versions of request data this controller supports.
27
+ * @var Array
28
+ */
29
+ protected $supportedVersions = [
30
+ '2016-03-01',
31
+ ];
32
+
33
+ public function processRequest(PriceWaiter_NYPWidget_Controller_Endpoint_Request $request)
34
+ {
35
+ $body = $request->getBody();
36
+ $id = isset($body->id) ? $body->id : null;
37
+
38
+ // Find the Deal
39
+ $deal = Mage::getModel('nypwidget/deal')->load($id);
40
+
41
+ if (!$deal->getId()) {
42
+ throw new PriceWaiter_NYPWidget_Exception_DealNotFound();
43
+ }
44
+
45
+ $deal->processRevokeRequest($request);
46
+
47
+ return PriceWaiter_NYPWidget_Controller_Endpoint_Response::ok();
48
+ }
49
+
50
+ }
app/code/community/PriceWaiter/NYPWidget/etc/config.xml CHANGED
@@ -3,7 +3,7 @@
3
 
4
  <modules>
5
  <PriceWaiter_NYPWidget>
6
- <version>2.5.5</version>
7
  </PriceWaiter_NYPWidget>
8
  </modules>
9
 
@@ -22,6 +22,12 @@
22
  <order>
23
  <table>nypwidget_orders</table>
24
  </order>
 
 
 
 
 
 
25
  </entities>
26
  </nypwidget_mysql4>
27
  <nypwidget_category>
@@ -84,7 +90,38 @@
84
  </catalog_category_prepare_save_pricewaiter>
85
  </observers>
86
  </catalog_category_prepare_save>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  </events>
 
 
 
 
 
 
 
 
 
 
 
88
  </global>
89
 
90
  <frontend>
@@ -103,6 +140,13 @@
103
  <frontName>pricewaiter</frontName>
104
  </args>
105
  </nypwidget>
 
 
 
 
 
 
 
106
  </routers>
107
  </frontend>
108
 
3
 
4
  <modules>
5
  <PriceWaiter_NYPWidget>
6
+ <version>3.0.0</version>
7
  </PriceWaiter_NYPWidget>
8
  </modules>
9
 
22
  <order>
23
  <table>nypwidget_orders</table>
24
  </order>
25
+ <deal>
26
+ <table>nypwidget_deal</table>
27
+ </deal>
28
+ <deal_usage>
29
+ <table>nypwidget_deal_usage</table>
30
+ </deal_usage>
31
  </entities>
32
  </nypwidget_mysql4>
33
  <nypwidget_category>
90
  </catalog_category_prepare_save_pricewaiter>
91
  </observers>
92
  </catalog_category_prepare_save>
93
+ <customer_logout>
94
+ <observers>
95
+ <customer_logout_pricewaiter>
96
+ <type>model</type>
97
+ <class>nypwidget/observer</class>
98
+ <method>handleCustomerLogout</method>
99
+ <args></args>
100
+ </customer_logout_pricewaiter>
101
+ </observers>
102
+ </customer_logout>
103
+ <sales_model_service_quote_submit_success>
104
+ <observers>
105
+ <sales_model_service_quote_submit_success_pricewaiter>
106
+ <type>model</type>
107
+ <class>nypwidget/observer</class>
108
+ <method>tieOrderToPriceWaiterDeals</method>
109
+ <args></args>
110
+ </sales_model_service_quote_submit_success_pricewaiter>
111
+ </observers>
112
+ </sales_model_service_quote_submit_success>
113
  </events>
114
+ <sales>
115
+ <quote>
116
+ <totals>
117
+ <pricewaiter>
118
+ <class>nypwidget/total_quote</class>
119
+ <after>discount,subtotal</after>
120
+ <before>grand_total</before>
121
+ </pricewaiter>
122
+ </totals>
123
+ </quote>
124
+ </sales>
125
  </global>
126
 
127
  <frontend>
140
  <frontName>pricewaiter</frontName>
141
  </args>
142
  </nypwidget>
143
+ <pricewaiter_deal>
144
+ <use>standard</use>
145
+ <args>
146
+ <module>PriceWaiter_NYPWidget</module>
147
+ <frontName>_dealpw</frontName>
148
+ </args>
149
+ </pricewaiter_deal>
150
  </routers>
151
  </frontend>
152
 
app/code/community/PriceWaiter/NYPWidget/sql/nypwidget_setup/mysql4-install-1.0.0.php CHANGED
@@ -49,3 +49,4 @@ CREATE TABLE {$this->getTable('nypwidget_category')} (
49
  // ->updateAttributes(array($product->getId()), array('nypwidget_enabled' => 1), 0);
50
  // }
51
 
 
49
  // ->updateAttributes(array($product->getId()), array('nypwidget_enabled' => 1), 0);
50
  // }
51
 
52
+ ?>
app/code/community/PriceWaiter/NYPWidget/sql/nypwidget_setup/mysql4-upgrade-2.5.4-3.0.0.php ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ $installer = $this;
4
+ $installer->startSetup();
5
+
6
+ $connection = $installer->getConnection();
7
+
8
+ $dealsTable = $connection
9
+ ->newTable($installer->getTable('nypwidget/deal'))
10
+ ->addColumn(
11
+ 'deal_id',
12
+ Varien_Db_Ddl_Table::TYPE_VARCHAR,
13
+ 36,
14
+ array(
15
+ 'nullable' => false,
16
+ 'primary' => true,
17
+ ),
18
+ 'Unique Deal ID'
19
+ )
20
+ ->addColumn(
21
+ 'revoked',
22
+ Varien_Db_Ddl_Table::TYPE_BOOLEAN,
23
+ null,
24
+ array(
25
+ 'default' => 0,
26
+ 'nullable' => false,
27
+ ),
28
+ 'Whether this deal has been revoked'
29
+ )
30
+ ->addColumn(
31
+ 'store_id',
32
+ Varien_Db_Ddl_Table::TYPE_INTEGER,
33
+ 10,
34
+ array(
35
+ 'nullable' => false,
36
+ 'unsigned' => true,
37
+ ),
38
+ 'Store this deal was for'
39
+ )
40
+ ->addColumn(
41
+ 'create_request_id',
42
+ Varien_Db_Ddl_Table::TYPE_VARCHAR,
43
+ 36,
44
+ array(
45
+ 'nullable' => false,
46
+ ),
47
+ 'X-PriceWaiter-Request-Id header from create call'
48
+ )
49
+ ->addColumn(
50
+ 'created_at',
51
+ Varien_Db_Ddl_Table::TYPE_DATETIME,
52
+ null,
53
+ array(
54
+ 'nullable' => false,
55
+ ),
56
+ 'Create date (UTC)'
57
+ )
58
+ ->addColumn(
59
+ 'revoke_request_id',
60
+ Varien_Db_Ddl_Table::TYPE_VARCHAR,
61
+ 36,
62
+ array(
63
+ 'nullable' => true,
64
+ ),
65
+ 'X-PriceWaiter-Request-Id header from revoke call'
66
+ )
67
+ ->addColumn(
68
+ 'revoked_at',
69
+ Varien_Db_Ddl_Table::TYPE_DATETIME,
70
+ null,
71
+ array(
72
+ 'nullable' => true,
73
+ ),
74
+ 'Revoke date (UTC)'
75
+ )
76
+ ->addColumn(
77
+ 'create_request_body_json',
78
+ Varien_Db_Ddl_Table::TYPE_TEXT,
79
+ null,
80
+ array(
81
+ 'nullable' => false,
82
+ ),
83
+ 'Full JSON for the create deal request.'
84
+ )
85
+ ->addColumn(
86
+ 'expires_at',
87
+ Varien_Db_Ddl_Table::TYPE_DATETIME,
88
+ null,
89
+ array(
90
+ 'nullable' => true,
91
+ ),
92
+ 'Expiry date (UTC)'
93
+ )
94
+ ->addColumn(
95
+ 'pricewaiter_buyer_id',
96
+ Varien_Db_Ddl_Table::TYPE_VARCHAR,
97
+ 36,
98
+ array(
99
+ 'nullable' => false,
100
+ ),
101
+ 'PriceWaiter buyer id'
102
+ )
103
+ ->addColumn(
104
+ 'order_id',
105
+ Varien_Db_Ddl_Table::TYPE_INTEGER,
106
+ 10,
107
+ array(
108
+ 'nullable' => true,
109
+ 'unsigned' => true,
110
+ ),
111
+ 'Magento order id deal was used on (if any)'
112
+ )
113
+ // Add an index for quick lookup when applying deals
114
+ ->addIndex(
115
+ $installer->getIdxName(array(
116
+ 'pricewaiter_buyer_id',
117
+ 'revoked',
118
+ 'order_id',
119
+ 'expires_at',
120
+ )),
121
+ array(
122
+ 'pricewaiter_buyer_id',
123
+ 'revoked',
124
+ 'order_id',
125
+ 'expires_at',
126
+ ),
127
+ array(
128
+ 'type' => Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX,
129
+ )
130
+ )
131
+ // Index on just order_id for quicker order listing
132
+ ->addIndex(
133
+ $installer->getIdxName(array('order_id')),
134
+ array('order_id'),
135
+ array(
136
+ 'type' => Varien_Db_Adapter_Interface::INDEX_TYPE_INDEX,
137
+ )
138
+ )
139
+ ->setComment('PriceWaiter Deals');
140
+
141
+ $dealUsageTable = $connection
142
+ ->newTable($installer->getTable('nypwidget/deal_usage'))
143
+ ->setComment('Tracks PriceWaiter Deal usage on quotes')
144
+ ->addColumn(
145
+ 'deal_id',
146
+ Varien_Db_Ddl_Table::TYPE_VARCHAR,
147
+ 36,
148
+ array(
149
+ 'nullable' => false,
150
+ ),
151
+ 'PriceWaiter deal uuid'
152
+ )
153
+ ->addColumn(
154
+ 'quote_id',
155
+ Varien_Db_Ddl_Table::TYPE_INTEGER,
156
+ 10,
157
+ array(
158
+ 'nullable' => false,
159
+ 'unsigned' => true,
160
+ ),
161
+ 'Quote deal was used on'
162
+ )
163
+ ->addForeignKey(
164
+ $installer->getFkName(
165
+ 'nypwidget/deal_usage',
166
+ 'quote_id',
167
+ 'sales/quote',
168
+ 'entity_id'
169
+ ),
170
+ 'quote_id',
171
+ $installer->getTable('sales/quote'),
172
+ 'entity_id',
173
+ // Cascade deletes when quote table is cleaned up
174
+ Varien_Db_Ddl_Table::ACTION_CASCADE
175
+ )
176
+ ->addIndex(
177
+ $installer->getIdxName(array(
178
+ 'deal_id',
179
+ 'quote_id',
180
+ )),
181
+ array(
182
+ 'deal_id',
183
+ 'quote_id',
184
+ ),
185
+ array(
186
+ 'type' => Varien_Db_Adapter_Interface::INDEX_TYPE_PRIMARY,
187
+ )
188
+ );
189
+
190
+ $connection->createTable($dealsTable);
191
+ $connection->createTable($dealUsageTable);
192
+
193
+ $installer->endSetup();
app/design/frontend/base/default/template/pricewaiter/widget.phtml CHANGED
@@ -1,84 +1,15 @@
1
- <?php $_product = Mage::registry('current_product'); ?>
2
- <?php if ($_product->isSaleable()): ?>
3
- <?php
4
- $customCss = Mage::getStoreConfig('pricewaiter/appearance/display_custom_css');
5
- $helper = $this->_getHelper();
6
 
7
- $magentoVersion = Mage::getVersion();
8
- $extensionVersion = Mage::getConfig()->getNode()->modules->PriceWaiter_NYPWidget->version;
9
 
10
- $brand = $helper->getProductBrand($_product);
11
-
12
- $image = $_product->getImageUrl();
13
- $currency = Mage::app()->getStore()->getCurrentCurrencyCode();
14
- if ($_product->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_GROUPED) {
15
- $groupedInformation = $helper->getGroupedProductInfo();
16
- } else {
17
- $groupedInformation = '';
18
- }
19
- if ($helper->isEnabledForStore()):
20
- ?>
21
- <script type="text/javascript">
22
- //<![CDATA[
23
- <?php echo $groupedInformation; ?>
24
- var PriceWaiterOptions = {
25
- <?php if ($helper->isButtonEnabled()): ?>
26
- enableButton: true,
27
- <?php else: ?>
28
- enableButton: false,
29
- <?php endif; ?>
30
- currency: '<?php echo $currency; ?>',
31
- <?php if ($helper->isConversionToolsEnabled()): ?>
32
- enableConversionTools: true,
33
- <?php else: ?>
34
- enableConversionTools: false,
35
- <?php endif; ?>
36
-
37
- product: {
38
- <?php if ($brand): ?>
39
- brand: <?php echo json_encode($brand); ?>,
40
- <?php endif; ?>
41
- sku: <?php echo json_encode($_product->getSku()); ?>,
42
- name: <?php echo json_encode($_product->getName()); ?>,
43
- price: <?php echo json_encode($helper->getProductPrice($_product)); ?>,
44
- <?php if ($image != ''): ?>
45
- image: <?php echo json_encode($image); ?>
46
- <?php endif; ?>
47
- },
48
- metadata: {
49
- _magento_version: "<?php echo $magentoVersion; ?>",
50
- _magento_extention_version: "<?php echo $extensionVersion; ?>"
51
- }
52
- };
53
-
54
- var PriceWaiterCategories = <?php echo $helper->getCategoriesAsJSON($_product); ?>;
55
-
56
- var PriceWaiterRegularPrice = '<?php echo (float) $_product->getPrice(); ?>';
57
- var PriceWaiterProductType = '<?php echo $_product->getTypeId(); ?>';
58
- //]]>
59
- </script>
60
- <div class="name-your-price-widget"
61
- style='display: block; clear: both; padding-top: 10px; <?php echo $customCss; ?>'>
62
  <span id="pricewaiter"></span>
63
  </div>
64
- <?php endif; ?>
65
- <?php if ($_product->getTypeId() == Mage_Catalog_Model_Product_Type::TYPE_CONFIGURABLE): ?>
66
- <?php
67
- $simples = Mage::getModel('catalog/product_type_configurable')->setProduct($_product)
68
- ->getUsedProductCollection()
69
- ->addAttributeToSelect('sku');
70
-
71
- $idToSku = array();
72
- foreach ($simples as $simple) {
73
- $idToSku[$simple->getId()] = $simple->getSku();
74
- } ?>
75
- <?php if (count($idToSku) > 0): ?>
76
- <script type="text/javascript">
77
- var PriceWaiterIdToSkus = <?php echo json_encode($idToSku); ?>;
78
- </script>
79
- <?php endif; ?>
80
- <?php endif; ?>
81
-
82
- <script src="<?php echo $helper->getWidgetUrl(); ?>" async></script>
83
 
84
- <?php endif; ?>
1
+ <?php
 
 
 
 
2
 
3
+ $embed = $this->getEmbed();
 
4
 
5
+ if ($embed->shouldRenderButtonPlaceholder()) {
6
+ echo '
7
+ <div
8
+ class="name-your-price-widget"
9
+ style="display: block; clear: both; padding-top: 10px;">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  <span id="pricewaiter"></span>
11
  </div>
12
+ ';
13
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ echo implode(PHP_EOL, $embed->getScriptTags());
package.xml CHANGED
@@ -1,14 +1,14 @@
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>nypwidget</name>
4
- <version>2.5.5</version>
5
  <stability>stable</stability>
6
  <license uri="http://www.apache.org/licenses/LICENSE-2.0.html">Apache License, Version 2.0</license>
7
  <channel>community</channel>
8
  <extends/>
9
  <summary>PriceWaiter lets buyers make offers on your products that you can easily accept, counter offer or reject.</summary>
10
- <description>Sell more, immediately, with the PriceWaiter widget. Convert comparison shoppers you might have lost, increase sales and conversions, and stop "minimum advertised price" from preventing sales before they even start.&#xD;&#xD; PriceWaiter lets customers make offers on products you sell-- offers you can accept, reject or counter. The widget embeds a simple Name Your Price button on any product page or category that you choose. Simply install the extension and you'll have full control over PriceWaiter in your Magento admin control panel.</description>
11
- <notes>Minor compatibility fixes.</notes>
12
  <authors>
13
  <author>
14
  <name>PriceWaiter</name>
@@ -16,9 +16,9 @@
16
  <email>extensions@pricewaiter.com</email>
17
  </author>
18
  </authors>
19
- <date>2016-10-21</date>
20
- <time>21:52:52</time>
21
- <contents><target name="mageweb"><dir name="app"><dir name="code"><dir name="community"><dir name="PriceWaiter"><file name=".DS_Store" hash="2a209fa87a026f2f0ca9c35ac878a3c6"/><dir name="NYPWidget"><file name=".DS_Store" hash="4b6f2307f11e3f2f15c48e055cb7c96a"/><dir name="Block"><dir name="Adminhtml"><file name="Link.php" hash="f17d6461e82c76a3a49266aa6a6b23e4"/><file name="Signup.php" hash="f3464994c972a3f0dcc7747ed011bbff"/><file name="Widget.php" hash="a5a15261dae201aca09a8249fe7e8a81"/></dir><file name="Category.php" hash="1047b4af8b7e438964dff8d82e7d8cf6"/><file name="Widget.php" hash="b6b1d0eab14ccb31a06971d6b824ed45"/><dir name="Payment"><dir name="Info"><file name="Pricewaiter.php" hash="ea9b94211a536552689d95c1664894e7"/></dir></dir></dir><dir name="controllers"><dir name="Adminhtml"><file name="PricewaiterController.php" hash="2caeb61607d9f0b7f873f1825c23e21f"/></dir><file name="CallbackController.php" hash="efedef40a3b35220216eeabbd3ae3567"/><file name="ProductinfoController.php" hash="b3af2cdacd4c820bfa26f37377efc84d"/></dir><dir name="etc"><file name="adminhtml.xml" hash="57af207c7a6966d365ed41ebe6d88ecb"/><file name="config.xml" hash="57a3919940380b71244556bcf7fc12a8"/><file name="system.xml" hash="3d83e0edf292b6e653d30f05382f1d9b"/></dir><dir name="Exception"><file name="Abstract.php" hash="d19b842ccbefffb2132c957b7feaf3e1"/><file name="ApiKey.php" hash="0cb0715d3242092d5e1fbecb8d978f59"/><file name="DuplicateOrder.php" hash="91eb14444c1bb6e01ddf2a78b92419c9"/><file name="InvalidOrderData.php" hash="6bbb8a438f1464acae9077b7d00f197c"/><file name="InvalidRegion.php" hash="02b69fca49a6584999d979816afca19d"/><file name="OutOfStock.php" hash="767b26a3d548ad92d86048f42910af6f"/><file name="Signature.php" hash="5244f5902f9141a189df78f696814a63"/><dir name="Product"><file name="Abstract.php" hash="1cb077cd79f7185657efb09ff285c7e9"/><file name="NotFound.php" hash="8e1507ab1798d85ce722705c913da441"/></dir></dir><dir name="Helper"><file name="Data.php" hash="69ef8411e6f6b3ef2d5424c24b893fb8"/><file name="Product.php" hash="3b8109ecfd93ba1aafa04ea4e878e8dc"/></dir><dir name="Model"><dir name="Callback"><file name="Inventory.php" hash="0be9afd71ec20b87ba29321f9fdc3cb8"/></dir><file name="Callback.php" hash="30a7cd95663d12f85bce4941c27ed5bb"/><file name="Category.php" hash="18826308eba00faaeffea8b473198ca4"/><file name="Observer.php" hash="0acd80de4ceb912ed8e7d48b017644a8"/><file name="Order.php" hash="86206e5678929e57b833f7ea4bc6bf96"/><file name="PaymentMethod.php" hash="0fa5e9a67512f578226e916b319052b7"/><dir name="Carrier"><file name="ShippingMethod.php" hash="93baf3d128481be22154b103cdf5d627"/></dir><dir name="Display"><file name="Phrase.php" hash="54339c37d5d88f0b2e68dfbe814af2e2"/><file name="Size.php" hash="2a70f37e54202f4a8de5a3f0b376f090"/></dir><dir name="Mysql4"><dir name="Category"><file name="Collection.php" hash="a5113a9db28d82cedcc8eb2c9b609a88"/></dir><file name="Category.php" hash="656c556879e61bbb20733ffc1dbe38d0"/><file name="Order.php" hash="ebecab2ef597171207ae787b0ddb68df"/><dir name="Order"><file name="Collection.php" hash="07773d6ca5bdf74d14e2d3290466b28f"/></dir></dir><dir name="Resource"><dir name="Eav"><dir name="Mysql4"><file name="Setup.php" hash="c23a03f23524957235bb3cbd1363551b"/></dir></dir></dir></dir><dir name="sql"><dir name="nypwidget_setup"><file name="mysql4-install-1.0.0.php" hash="95804a9cbe9a668ffd0d67517bdaa240"/><file name="mysql4-upgrade-1.1.2-1.1.3.php" hash="df453265b32071329de809aa750dd31f"/><file name="mysql4-upgrade-1.1.7-1.1.8.php" hash="9b18e12698cbdc6896666b26362a7ddb"/><file name="mysql4-upgrade-1.2.4-1.2.5.php" hash="5e501f27bf223c34e19443923e998021"/><file name="mysql4-upgrade-1.3.0-1.3.1.php" hash="4cd7633a027696aa74668c61c58a83e2"/><file name="mysql4-upgrade-2.1.5-2.2.0.php" hash="be3f748105ff6bf61d4329b967c09c94"/><file name="mysql4-upgrade-2.2.0-2.5.0.php" hash="c02dacf91e070d7274daeda881e68f2a"/></dir></dir></dir></dir></dir></dir><dir name="design"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="layout"><file name="pricewaiter.xml" hash="646560535c94bf42ed75438ec9cc6b9f"/></dir><dir name="template"><dir name="pricewaiter"><file name="categorytab.phtml" hash="703fcf0daa0fb71bef23987a9896bd29"/><file name="signup.phtml" hash="fc18c669c18c55cccdf2c66c96199dec"/></dir></dir></dir></dir></dir><dir name="frontend"><dir name="base"><dir name="default"><dir name="layout"><file name="pricewaiter.xml" hash="d578d936a726cca4837d5f2e58c5ca45"/></dir><dir name="template"><dir name="pricewaiter"><file name="widget.phtml" hash="1035d7df739b2a80a581b13684f95b39"/></dir></dir></dir></dir></dir></dir><dir name="etc"><dir name="modules"><file name="PriceWaiter_NYPWidget.xml" hash="7649918eb71009656f956a077f0cf6a8"/></dir></dir></dir><dir name="js"><dir name="pricewaiter"><file name="product-pages.js" hash="775a2c04db1f58c50f204ed7e15aef76"/><file name="token.js" hash="79ce2bdf4d9ae33fb4ef38c298f2dffe"/></dir></dir><dir name="skin"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="images"><file name="pricewaiter_logo.png" hash="becb9713a561131ff69eabb7503d3e74"/><file name="pricewaiter_tab.png" hash="1bab71be6b1a93aee2ae7aeae3807484"/><file name="pricewaiter_logo.png" hash="becb9713a561131ff69eabb7503d3e74"/><file name="pricewaiter_tab.png" hash="1bab71be6b1a93aee2ae7aeae3807484"/></dir><file name="pricewaiter.css" hash="cbda20d51ec4c1505962c1e79007d7f7"/></dir></dir></dir></dir></target></contents>
22
  <compatible/>
23
  <dependencies>
24
  <required>
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>nypwidget</name>
4
+ <version>3.0.0</version>
5
  <stability>stable</stability>
6
  <license uri="http://www.apache.org/licenses/LICENSE-2.0.html">Apache License, Version 2.0</license>
7
  <channel>community</channel>
8
  <extends/>
9
  <summary>PriceWaiter lets buyers make offers on your products that you can easily accept, counter offer or reject.</summary>
10
+ <description>Sell more, immediately, with PriceWaiter. Convert comparison shoppers you might have lost, increase sales and conversions, and stop "minimum advertised price" from preventing sales before they even start.&#xD;&#xD; PriceWaiter lets customers make offers on products you sell-- offers you can accept, reject or counter. The widget embeds a simple Name Your Price button on any product page or category that you choose. Simply install the extension and you'll have full control over PriceWaiter in your Magento admin control panel.</description>
11
+ <notes>Adds support for checkout via Magento.</notes>
12
  <authors>
13
  <author>
14
  <name>PriceWaiter</name>
16
  <email>extensions@pricewaiter.com</email>
17
  </author>
18
  </authors>
19
+ <date>2016-11-21</date>
20
+ <time>20:55:50</time>
21
+ <contents><target name="mageweb"><dir name="app"><dir name="code"><dir name="community"><dir name="PriceWaiter"><file name=".DS_Store" hash="2a209fa87a026f2f0ca9c35ac878a3c6"/><dir name="NYPWidget"><file name=".DS_Store" hash="4b6f2307f11e3f2f15c48e055cb7c96a"/><dir name="Block"><dir name="Adminhtml"><file name="Link.php" hash="f17d6461e82c76a3a49266aa6a6b23e4"/><file name="Signup.php" hash="f3464994c972a3f0dcc7747ed011bbff"/><file name="Widget.php" hash="a5a15261dae201aca09a8249fe7e8a81"/></dir><file name="Category.php" hash="1047b4af8b7e438964dff8d82e7d8cf6"/><file name="Widget.php" hash="abeee723164cb607c5110aee2b12bc0a"/><dir name="Payment"><dir name="Info"><file name="Pricewaiter.php" hash="ea9b94211a536552689d95c1664894e7"/></dir></dir></dir><dir name="Controller"><dir name="Endpoint"><file name="Request.php" hash="8e706ce0dd68d2d45aa9fcfa0fd79d17"/><file name="Response.php" hash="f205fca4133184d6dc1c220f6da7946e"/></dir><file name="Endpoint.php" hash="2a462bffd94b57aa1955bac03b8433c7"/></dir><dir name="controllers"><dir name="Adminhtml"><file name="PricewaiterController.php" hash="29d6f14dc97fde5c5d1c5bb969692b9a"/></dir><file name="CallbackController.php" hash="25c5ab8b737df6e24bba57c8b46b74d5"/><file name="CheckoutController.php" hash="1050baec45534d63f824e9a931b3f824"/><file name="CreatedealController.php" hash="38edc9dc1c7935e1d0c1bd76d626bef8"/><file name="DebugController.php" hash="c9fa18395b1e649ccc795ad9f7cd5321"/><file name="ListordersController.php" hash="61d01b554778cd0e2f84ecf6e0af6f72"/><file name="PingController.php" hash="4103dd3758146f333b0c7e667c169ff3"/><file name="ProductinfoController.php" hash="160369f5d9c037f4ca1f13b3c579bc32"/><file name="RevokedealController.php" hash="da35b7ce683753d009164fea3ce564b2"/></dir><dir name="etc"><file name="adminhtml.xml" hash="57af207c7a6966d365ed41ebe6d88ecb"/><file name="config.xml" hash="f0afa04e0438dc4bbfd81e48323f9559"/><file name="system.xml" hash="3d83e0edf292b6e653d30f05382f1d9b"/></dir><dir name="Exception"><file name="Abstract.php" hash="6a86e327c5f5110bfa5249d92573a871"/><file name="ApiKey.php" hash="0cb0715d3242092d5e1fbecb8d978f59"/><file name="DealAlreadyCreated.php" hash="0fa91d8e9a2ef47d9039b589c2c8cc81"/><file name="DealAlreadyRevoked.php" hash="6de1fbb0df19f224923c615f13edb49b"/><file name="DealNotFound.php" hash="76e8da946df39482f2768d1eae36f790"/><file name="DuplicateOrder.php" hash="91eb14444c1bb6e01ddf2a78b92419c9"/><file name="InvalidOrderData.php" hash="6bbb8a438f1464acae9077b7d00f197c"/><file name="InvalidRegion.php" hash="e27b75878d290011b541b6cab032f116"/><file name="NoTestDeals.php" hash="f663a330d268cf43f14c41ca5c0a77bc"/><file name="OrderNotFound.php" hash="57eb66ac79ae7bd771a60ac7ae5e9da6"/><file name="Signature.php" hash="55e22f7459e76f4341e45ae3d8bb3d97"/><file name="SingleItemOnly.php" hash="12f83b8017cef267a595f5aabcbcea87"/><file name="Version.php" hash="7f2c0e0a398e7cde26c9d9649fbc09c3"/><dir name="Product"><file name="Abstract.php" hash="859a53ed0e6d3f9914044ec14aef6e47"/><file name="Invalid.php" hash="050fc42ad68af5ddeadd050df31c6028"/><file name="NotFound.php" hash="91e63e96104b9ecb79bdede2df23b4c1"/><file name="OutOfStock.php" hash="430c5d119a77cdda6323561aec065c22"/></dir></dir><dir name="Helper"><file name="About.php" hash="90a78f52efc937586cd370735d9672a5"/><file name="Data.php" hash="57caf1984bfb86ac0eaef4846dbcdebc"/></dir><dir name="Model"><dir name="Callback"><file name="Inventory.php" hash="6a69e883cc1403cfacfc40d7069490ac"/></dir><file name="Callback.php" hash="fb2b22cc32710efd35cacadb9ed941ca"/><file name="Category.php" hash="18826308eba00faaeffea8b473198ca4"/><file name="Deal.php" hash="7f1406b5d13d40634f6f917cab61e4b7"/><file name="Discounter.php" hash="5b6f0ea67a70917a963cf25736f957bb"/><file name="Embed.php" hash="e43299138e67613d3c7a4c220f47edde"/><file name="Observer.php" hash="a266be2b690e2914a213d916fef3ed9d"/><file name="Order.php" hash="86206e5678929e57b833f7ea4bc6bf96"/><file name="PaymentMethod.php" hash="0fa5e9a67512f578226e916b319052b7"/><file name="Session.php" hash="6c8c9fba62ba926d9af7b3c40a074d5c"/><dir name="Carrier"><file name="ShippingMethod.php" hash="93baf3d128481be22154b103cdf5d627"/></dir><dir name="Display"><file name="Phrase.php" hash="54339c37d5d88f0b2e68dfbe814af2e2"/><file name="Size.php" hash="2a70f37e54202f4a8de5a3f0b376f090"/></dir><dir name="Mysql4"><dir name="Category"><file name="Collection.php" hash="a5113a9db28d82cedcc8eb2c9b609a88"/></dir><file name="Category.php" hash="656c556879e61bbb20733ffc1dbe38d0"/><file name="Deal.php" hash="6d94a010e9e213057deb4677e5c166e0"/><file name="Order.php" hash="ebecab2ef597171207ae787b0ddb68df"/><dir name="Deal"><file name="Collection.php" hash="4bdd3c43fee16b80c9188af232c5e9ef"/><file name="Usage.php" hash="29193d08ab3054c0e61ab38da288d452"/></dir><dir name="Order"><file name="Collection.php" hash="07773d6ca5bdf74d14e2d3290466b28f"/></dir></dir><dir name="Offer"><dir name="Item"><file name="Handler.php" hash="1704e8065802116706a3fd2f1842fb94"/><file name="Inventory.php" hash="b518984933c386f7dba06305bb3652ee"/><file name="Pricing.php" hash="ca342c391bd18bf199afef6def2902af"/></dir><file name="Item.php" hash="bc4306df349ecac42b88e510a69c184a"/></dir><dir name="Resource"><dir name="Eav"><dir name="Mysql4"><file name="Setup.php" hash="c23a03f23524957235bb3cbd1363551b"/></dir></dir></dir><dir name="Total"><file name="Quote.php" hash="7354457003e0cb0cfc4f7bc129e6f4a3"/></dir></dir><dir name="sql"><dir name="nypwidget_setup"><file name="mysql4-install-1.0.0.php" hash="b977c3a62af93441d115650b543ff858"/><file name="mysql4-upgrade-1.1.2-1.1.3.php" hash="df453265b32071329de809aa750dd31f"/><file name="mysql4-upgrade-1.1.7-1.1.8.php" hash="9b18e12698cbdc6896666b26362a7ddb"/><file name="mysql4-upgrade-1.2.4-1.2.5.php" hash="5e501f27bf223c34e19443923e998021"/><file name="mysql4-upgrade-1.3.0-1.3.1.php" hash="4cd7633a027696aa74668c61c58a83e2"/><file name="mysql4-upgrade-2.1.5-2.2.0.php" hash="be3f748105ff6bf61d4329b967c09c94"/><file name="mysql4-upgrade-2.2.0-2.5.0.php" hash="c02dacf91e070d7274daeda881e68f2a"/><file name="mysql4-upgrade-2.5.4-3.0.0.php" hash="c2366841c23fb9242b5332fad1615522"/></dir></dir></dir></dir></dir></dir><dir name="design"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="layout"><file name="pricewaiter.xml" hash="646560535c94bf42ed75438ec9cc6b9f"/></dir><dir name="template"><dir name="pricewaiter"><file name="categorytab.phtml" hash="703fcf0daa0fb71bef23987a9896bd29"/><file name="signup.phtml" hash="fc18c669c18c55cccdf2c66c96199dec"/></dir></dir></dir></dir></dir><dir name="frontend"><dir name="base"><dir name="default"><dir name="layout"><file name="pricewaiter.xml" hash="d578d936a726cca4837d5f2e58c5ca45"/></dir><dir name="template"><dir name="pricewaiter"><file name="widget.phtml" hash="018ad07f442de5837317ecfac18ab01d"/></dir></dir></dir></dir></dir></dir><dir name="etc"><dir name="modules"><file name="PriceWaiter_NYPWidget.xml" hash="7649918eb71009656f956a077f0cf6a8"/></dir></dir></dir><dir name="js"><dir name="pricewaiter"><file name="product-pages.js" hash="775a2c04db1f58c50f204ed7e15aef76"/><file name="token.js" hash="79ce2bdf4d9ae33fb4ef38c298f2dffe"/></dir></dir><dir name="skin"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="images"><file name="pricewaiter_logo.png" hash="becb9713a561131ff69eabb7503d3e74"/><file name="pricewaiter_tab.png" hash="1bab71be6b1a93aee2ae7aeae3807484"/><file name="pricewaiter_logo.png" hash="becb9713a561131ff69eabb7503d3e74"/><file name="pricewaiter_tab.png" hash="1bab71be6b1a93aee2ae7aeae3807484"/></dir><file name="pricewaiter.css" hash="cbda20d51ec4c1505962c1e79007d7f7"/></dir></dir></dir></dir></target></contents>
22
  <compatible/>
23
  <dependencies>
24
  <required>