Mage_Exactor_Tax - Version 2012.09.20

Version Notes

Supported Magento 1.5.0.0 - 1.7.x, Magento Enterprise 1.8-1.12.x

Download this release

Release Info

Developer Exactor, Inc.
Extension Mage_Exactor_Tax
Version 2012.09.20
Comparing to
See all releases


Code changes from version 2012.07.23 to 2012.09.20

app/code/local/Exactor/Core/Helper/SessionCache.php CHANGED
@@ -37,6 +37,9 @@ class Exactor_Core_Helper_SessionCache extends Mage_Core_Helper_Abstract{
37
  return null;
38
  }
39
 
 
 
 
40
  public function popTransactionInfo(){
41
  $transactionsArray = $this->fetchAll();
42
  if (count($transactionsArray) == 0) return;
@@ -51,6 +54,7 @@ class Exactor_Core_Helper_SessionCache extends Mage_Core_Helper_Abstract{
51
  */
52
  public function clear(){
53
  $this->magentoSession->setExactorTransactionInfo(null);
 
54
  }
55
 
56
  /**
37
  return null;
38
  }
39
 
40
+ /**
41
+ * @return ExactorTransactionInfo
42
+ */
43
  public function popTransactionInfo(){
44
  $transactionsArray = $this->fetchAll();
45
  if (count($transactionsArray) == 0) return;
54
  */
55
  public function clear(){
56
  $this->magentoSession->setExactorTransactionInfo(null);
57
+ $this->magentoSession->setExactorTransactionInfoArray(null);
58
  }
59
 
60
  /**
app/code/local/Exactor/ExactorSettings/controllers/Adminhtml/FormController.php CHANGED
@@ -54,6 +54,7 @@ class Exactor_ExactorSettings_Adminhtml_FormController extends Mage_Adminhtml_Co
54
  require_once($libDir . '/XmlProcessing.php');
55
  require_once($libDir . '/ExactorDomainObjects.php');
56
  require_once($libDir . '/ExactorCommons.php');
 
57
  // Magento specific definitions
58
  require_once($libDir . '/Magento.php');
59
  require_once($libDir . '/config.php');
54
  require_once($libDir . '/XmlProcessing.php');
55
  require_once($libDir . '/ExactorDomainObjects.php');
56
  require_once($libDir . '/ExactorCommons.php');
57
+ require_once($libDir . '/RegionResolver.php');
58
  // Magento specific definitions
59
  require_once($libDir . '/Magento.php');
60
  require_once($libDir . '/config.php');
app/code/local/Exactor/Sales/Model/Observer.php CHANGED
@@ -11,10 +11,13 @@ class Exactor_Sales_Model_Observer {
11
  private $exactorSettingsHelper;
12
  /** @var Exactor_Core_Helper_SessionCache */
13
  private $sessionCache;
14
- /** @var Exactor_Core_Model_MerchantSettings */
15
- private $merchantSettings;
16
  /** @var ExactorProcessingService*/
17
  private $exactorProcessingService;
 
 
 
 
 
18
  /** @var IExactorLogger */
19
  private $logger;
20
 
@@ -23,6 +26,7 @@ class Exactor_Sales_Model_Observer {
23
  require_once($libDir . '/XmlProcessing.php');
24
  require_once($libDir . '/ExactorDomainObjects.php');
25
  require_once($libDir . '/ExactorCommons.php');
 
26
  // Magento specific definitions
27
  require_once($libDir . '/Magento.php');
28
  require_once($libDir . '/config.php');
@@ -34,6 +38,7 @@ class Exactor_Sales_Model_Observer {
34
  $this->logger = ExactorLoggingFactory::getInstance()->getLogger($this);
35
  $this->exactorSettingsHelper = Mage::helper('ExactorSettings');
36
  $this->sessionCache = Mage::helper('Exactor_Core_SessionCache/');
 
37
  }
38
 
39
  /**
@@ -73,34 +78,65 @@ class Exactor_Sales_Model_Observer {
73
  }
74
  }
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  public function handleAllOrdersCompleted(Varien_Event_Observer $observer){
 
77
  if (is_array($observer->getOrders()))
78
  $orders = array_reverse($observer->getOrders());
79
  else
80
  $orders = array($observer->getOrder());
81
  foreach($orders as $order){
82
- $merchantSettings = $this->loadMerchantSettings($order);
83
- if ($merchantSettings==null) continue;
84
- $transactionInfo = $this->sessionCache->popTransactionInfo($order);
85
- if ($transactionInfo == null){
86
- $this->logger->error('Nothing to process. There is no transaction in the session cache', 'handleCreatedOrder');
87
- return;
88
- }
89
- // Update transaction info with order information
90
- $orderId = $order->getIncrementId();
91
- $transactionInfo->setShoppingCartTrnId($orderId);
92
- // Push latest transaction from the Session to DB
93
- $this->exactorProcessingService->getPluginCallback()->saveTransactionInfo($transactionInfo,$transactionInfo->getSignature());
94
- // if CommitOption is set up to commit on sales order - do commit the
95
- // latest transaction from the session storage
96
- if ($merchantSettings->getCommitOption() == Exactor_Core_Model_MerchantSettings::COMMIT_ON_SALES_ORDER){
97
- $this->exactorProcessingService->commitExistingInvoiceForOrder($orderId);
98
- }
99
  }
100
  // We need to clean the session storage here
101
  $this->sessionCache->clear();
102
  }
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  /**
105
  * Event will be fired once new order has been created
106
  * @param Varien_Event_Observer $observer
@@ -108,22 +144,39 @@ class Exactor_Sales_Model_Observer {
108
  */
109
  public function handleCreatedOrder(Varien_Event_Observer $observer){
110
  $this->logger->trace('called', 'handleCreatedOrder');
111
- return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  }
113
 
114
  public function handleNewCreditMemo(Varien_Event_Observer $observer){
115
  $this->logger->trace('called', 'handleNewCreditMemo');
116
  $merchantSettings = $this->loadMerchantSettings($observer->getCreditmemo()->getOrder());
117
  if ($merchantSettings==null) return;
118
- $orderId = $observer->getCreditmemo()->getOrder()->getIncrementId();
119
- $this->refundTransactionForOrder($orderId);
120
  }
121
 
122
  public function handleCancelOrder(Varien_Event_Observer $observer){
123
  $this->logger->trace('called', 'handleCancelOrder');
124
- $merchantSettings = $this->loadMerchantSettings($observer->getOrder());
125
  if ($merchantSettings==null) return;
126
- $orderId = $observer->getOrder()->getIncrementId();
127
  $this->refundTransactionForOrder($orderId);
128
  }
129
 
11
  private $exactorSettingsHelper;
12
  /** @var Exactor_Core_Helper_SessionCache */
13
  private $sessionCache;
 
 
14
  /** @var ExactorProcessingService*/
15
  private $exactorProcessingService;
16
+ /**
17
+ * @var Exactor_Tax_Helper_Mapping
18
+ */
19
+ private $exactorMappingHelper;
20
+
21
  /** @var IExactorLogger */
22
  private $logger;
23
 
26
  require_once($libDir . '/XmlProcessing.php');
27
  require_once($libDir . '/ExactorDomainObjects.php');
28
  require_once($libDir . '/ExactorCommons.php');
29
+ require_once($libDir . '/RegionResolver.php');
30
  // Magento specific definitions
31
  require_once($libDir . '/Magento.php');
32
  require_once($libDir . '/config.php');
38
  $this->logger = ExactorLoggingFactory::getInstance()->getLogger($this);
39
  $this->exactorSettingsHelper = Mage::helper('ExactorSettings');
40
  $this->sessionCache = Mage::helper('Exactor_Core_SessionCache/');
41
+ $this->exactorMappingHelper = Mage::helper('tax/mapping');
42
  }
43
 
44
  /**
78
  }
79
  }
80
 
81
+ /**
82
+ * Stores order in our local DB, Commits transaction if needed.
83
+ * Returns false if for some reason operation wasn't successful.
84
+ *
85
+ * @param $order
86
+ * @return bool
87
+ */
88
+ private function processFinishedOrder($order){
89
+ $merchantSettings = $this->loadMerchantSettings($order);
90
+ if ($merchantSettings==null) false;
91
+ $transactionInfo = $this->sessionCache->popTransactionInfo($order);
92
+ if ($transactionInfo == null){
93
+ $this->logger->error('Nothing to process. There is no transaction in the session cache', 'handleAllOrdersCompleted');
94
+ return false;
95
+ }
96
+ // Update transaction info with order information
97
+ $orderId = $order->getIncrementId();
98
+ $transactionInfo->setShoppingCartTrnId($orderId);
99
+ // Push latest transaction from the Session to DB
100
+ $this->exactorProcessingService->getPluginCallback()->saveTransactionInfo($transactionInfo,$transactionInfo->getSignature());
101
+ // if CommitOption is set up to commit on sales order - do commit the
102
+ // latest transaction from the session storage
103
+ if ($merchantSettings->getCommitOption() == Exactor_Core_Model_MerchantSettings::COMMIT_ON_SALES_ORDER){
104
+ $this->logger->info("Commiting transaction for order $orderId - " . $transactionInfo->getExactorTrnId(), 'handleAllOrdersCompleted');
105
+ $this->exactorProcessingService->commitExistingInvoiceForOrder($orderId);
106
+ }
107
+ return true;
108
+ }
109
+
110
  public function handleAllOrdersCompleted(Varien_Event_Observer $observer){
111
+ $this->logger->trace('called', 'handleAllOrdersCompleted');
112
  if (is_array($observer->getOrders()))
113
  $orders = array_reverse($observer->getOrders());
114
  else
115
  $orders = array($observer->getOrder());
116
  foreach($orders as $order){
117
+ $this->processFinishedOrder($order);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  }
119
  // We need to clean the session storage here
120
  $this->sessionCache->clear();
121
  }
122
 
123
+ private function partialRefund(Mage_Sales_Model_Order_Creditmemo $creditMemo){
124
+ $transactionInfo = $this->exactorProcessingService->loadTransactionInfoByOrderId($creditMemo->getOrder()->getIncrementId());
125
+ if ($transactionInfo==null || !$transactionInfo->getIsCommited()){
126
+ $this->logger->info("Exactor transaction for order " .
127
+ $creditMemo->getOrder()->getIncrementId() . "doesn't exists or wasn't commited", 'partialRefund' );
128
+ return;
129
+ }
130
+ $merchantSettings = $this->loadMerchantSettings($creditMemo->getOrder());
131
+ if ($merchantSettings==null) return;
132
+ $invoiceRequests = $this->exactorMappingHelper->buildInvoiceRequestsForCreditMemo($creditMemo, $merchantSettings);
133
+ $exactorProcessingService = ExactorProcessingServiceFactory::getInstance()->buildExactorProcessingService($merchantSettings->getMerchantID(),
134
+ $merchantSettings->getUserID());
135
+ foreach ($invoiceRequests as $invoice) {
136
+ $exactorProcessingService->partialRefund($invoice, new DateTime(), $creditMemo->getOrder()->getIncrementId());
137
+ }
138
+ }
139
+
140
  /**
141
  * Event will be fired once new order has been created
142
  * @param Varien_Event_Observer $observer
144
  */
145
  public function handleCreatedOrder(Varien_Event_Observer $observer){
146
  $this->logger->trace('called', 'handleCreatedOrder');
147
+ if ($this->shouldOrderProcessingRunInCompatibilityMode($observer->getOrder())) {
148
+ $this->logger->info('Order processing started in compatibility mode.', 'handleCreatedOrder');
149
+ $this->processFinishedOrder($observer->getOrder());
150
+ $this->sessionCache->clear();
151
+ }
152
+ }
153
+
154
+ private function shouldOrderProcessingRunInCompatibilityMode($order) {
155
+ if ($order->getPayment() != null &&
156
+ strpos($order->getPayment()->getMethod(), "paypal_express") !== false){
157
+ $this->logger->info('PayPal order detected', 'shouldOrderProcessingRunInCompatibilityMode');
158
+ return true;
159
+ }
160
+ $mageVersionInfo = Mage::getVersionInfo();
161
+ if ($mageVersionInfo['major']==1 && $mageVersionInfo['minor'] == 8){
162
+ $this->logger->info('Magento Enterprise detected', 'shouldOrderProcessingRunInCompatibilityMode');
163
+ return true;
164
+ }
165
+ return false;
166
  }
167
 
168
  public function handleNewCreditMemo(Varien_Event_Observer $observer){
169
  $this->logger->trace('called', 'handleNewCreditMemo');
170
  $merchantSettings = $this->loadMerchantSettings($observer->getCreditmemo()->getOrder());
171
  if ($merchantSettings==null) return;
172
+ $this->partialRefund($observer->getCreditmemo());
 
173
  }
174
 
175
  public function handleCancelOrder(Varien_Event_Observer $observer){
176
  $this->logger->trace('called', 'handleCancelOrder');
177
+ $merchantSettings = $this->loadMerchantSettings($observer->getPayment()->getOrder());
178
  if ($merchantSettings==null) return;
179
+ $orderId = $observer->getPayment()->getOrder()->getIncrementId();
180
  $this->refundTransactionForOrder($orderId);
181
  }
182
 
app/code/local/Exactor/Sales/etc/config.xml CHANGED
@@ -28,7 +28,7 @@
28
  <config>
29
  <modules>
30
  <Exactor_Sales>
31
- <version>2012.06.23</version>
32
  </Exactor_Sales>
33
  </modules>
34
  <global>
@@ -60,7 +60,7 @@
60
  </exactor_sales_observer>
61
  </observers>
62
  </sales_order_creditmemo_refund>
63
- <order_cancel_after>
64
  <observers>
65
  <exactor_sales_observer>
66
  <type>singleton</type>
@@ -68,7 +68,7 @@
68
  <method>handleCancelOrder</method>
69
  </exactor_sales_observer>
70
  </observers>
71
- </order_cancel_after>
72
  <sales_order_shipment_save_after>
73
  <observers>
74
  <exactor_sales_observer>
@@ -88,5 +88,12 @@
88
  </observers>
89
  </sales_order_invoice_pay>
90
  </events>
 
 
 
 
 
 
 
91
  </global>
92
  </config>
28
  <config>
29
  <modules>
30
  <Exactor_Sales>
31
+ <version>2012.08.28</version>
32
  </Exactor_Sales>
33
  </modules>
34
  <global>
60
  </exactor_sales_observer>
61
  </observers>
62
  </sales_order_creditmemo_refund>
63
+ <sales_order_payment_cancel>
64
  <observers>
65
  <exactor_sales_observer>
66
  <type>singleton</type>
68
  <method>handleCancelOrder</method>
69
  </exactor_sales_observer>
70
  </observers>
71
+ </sales_order_payment_cancel>
72
  <sales_order_shipment_save_after>
73
  <observers>
74
  <exactor_sales_observer>
88
  </observers>
89
  </sales_order_invoice_pay>
90
  </events>
91
+ <models>
92
+ <tax>
93
+ <rewrite>
94
+ <sales_model_order_creditmemo_total_tax>Exactor_Sales_Model_Order_Creditmemo_Total_Tax</sales_model_order_creditmemo_total_tax>
95
+ </rewrite>
96
+ </tax>
97
+ </models>
98
  </global>
99
  </config>
app/code/local/Exactor/Tax/Helper/Mapping.php CHANGED
@@ -11,15 +11,21 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
11
  const EUC_SHIPPING_USPS='EUC-13030202';
12
  const EUC_SHIPPING_AND_HANDLING='EUC-13010101';
13
  const EUC_HANDLING = 'EUC-13010301';
 
 
14
 
15
  const MSG_DEFAULT_SHIPPING_NAME = 'Default Shipping';
16
  const MSG_HANDLING_FEE = 'Handling Fee';
 
17
  const MSG_SHIPPING_DESCRIPTION_PREFIX = 'Shipping Fee: ';
18
  const MSG_ESTIMATION_REQUEST = 'Magento Tax Estimation Request';
19
  const MSG_DISCOUNTED_BY = 'Discounted by $';
 
 
20
 
21
  const LINE_ITEM_ID_SHIPPING = "SHIPPING";
22
  const LINE_ITEM_ID_HANDLING = "HANDLING";
 
23
  const INDEXED_LINE_ITEM_ID_PREFIX = '_';
24
 
25
  const ATTRIBUTE_NAME_EXEMPTION = 'taxvat';
@@ -32,8 +38,11 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
32
  const PO_ESTIMATE_TEXT = 'Estimated Tax ';
33
 
34
  const UNKNOWN_STREET_TEXT = "";
 
 
35
 
36
 
 
37
 
38
  private function getLogger(){
39
  if ($this->logger==null)
@@ -57,7 +66,7 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
57
  return join(' ', $parts);
58
  }
59
 
60
- public function buildExactorAddressForQuoteAddress(Mage_Sales_Model_Quote_Address $address){
61
  $exactorAddress = new AddressType();
62
  if ($address==null) return null;
63
  // Set defaults
@@ -65,7 +74,7 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
65
  $exactorAddress->setFullName("Unknown Buyer");
66
  //
67
  $fullName = trim($address->getName());//trim($this->buildFullName($address->getFirstname(), $address->getLastname(), $address->getMiddlename()));
68
- if (strlen($fullName)>0)
69
  $exactorAddress->setFullName($fullName);
70
  if ($address->getStreetFull() != null)
71
  $exactorAddress->setStreet1($address->getStreetFull());
@@ -76,27 +85,45 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
76
  return $exactorAddress;
77
  }
78
 
79
- public function getSKUForItem(Mage_Sales_Model_Quote_Item $magentoItem,
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  Exactor_Core_Model_MerchantSettings $merchantSettings){
81
  $sku='';
 
 
 
 
 
82
  switch ($merchantSettings->getSourceOfSKU()){
83
  case Exactor_Core_Model_MerchantSettings::SKU_SOURCE_NONE:
84
  $sku = '';
85
  break;
86
  case Exactor_Core_Model_MerchantSettings::SKU_SOURCE_SKU_FIELD:
87
- $sku = $magentoItem->getSku();;
88
  break;
89
  case Exactor_Core_Model_MerchantSettings::SKU_SOURCE_ATTRIBUTE_NAME:
90
  $attributeSetName = 'Default';
91
  try{
92
  $attributeSetModel = Mage::getModel("eav/entity_attribute_set");
93
- $attributeSetModel->load($magentoItem->getProduct()->getAttributeSetId());
94
  $attributeSetName = $attributeSetModel->getAttributeSetName();
95
  }catch(Exception $e){}
96
  $sku = $attributeSetName;
97
  break;
98
  case Exactor_Core_Model_MerchantSettings::SKU_SOURCE_PRODUCT_CATEGORY:
99
- $category = $magentoItem->getProduct()->getCategory();
100
  if ($category != null)
101
  $sku = $category->getName();
102
  break;
@@ -104,11 +131,11 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
104
  /** @var Mage_Tax_Model_Mysql4_Class_Collection $taxClassCollection */
105
  $taxClassCollection = Mage::getModel('tax/class')->getCollection();
106
  /** @var Mage_Tax_Model_Class $taxClass */
107
- $taxClass = $taxClassCollection->getItemById($magentoItem->getProduct()->getTaxClassId());
108
  if ($taxClass == null) $sku = ''; else $sku = $taxClass->getClassName();
109
  break;
110
  }
111
- return substr($sku,0, self::MAX_SKU_CODE_LENGTH); // Max length for SKU is 16 characters
112
  }
113
 
114
  private function isUSPSShipping($methodName){
@@ -121,20 +148,40 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
121
  return false;
122
  }
123
 
124
- public function getShippingLineItem(Mage_Sales_Model_Quote_Address $quoteAddress,
125
  Exactor_Core_Model_MerchantSettings $merchantSettings){
 
126
  if ($quoteAddress->getAddressType() == Mage_Sales_Model_Quote_Address::TYPE_BILLING) return null; // There is no shipping fees there
127
- if ($quoteAddress->getShippingAmount()==0) return null;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  $shippingLineItem = new LineItemType();
129
- $shippingLineItem->setDescription(self::MSG_SHIPPING_DESCRIPTION_PREFIX . $quoteAddress->getShippingDescription());
130
  if (trim($shippingLineItem->getDescription())==''){
131
  $shippingLineItem->setDescription(self::MSG_DEFAULT_SHIPPING_NAME);
132
- }
133
  // Get EUC code for shipping
134
  $shippingEUC = self::EUC_SHIPPING_COMMON_CARRIER;
135
  if ($merchantSettings->isShippingIncludeHandling()){
136
  $shippingEUC = self::EUC_SHIPPING_AND_HANDLING;
137
- }else if ($this->isUSPSShipping($quoteAddress->getShippingDescription())){
138
  $shippingEUC = self::EUC_SHIPPING_USPS;
139
  }
140
  $shippingLineItem->setSKU($shippingEUC);
@@ -142,55 +189,61 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
142
  $shippingLineItem->setId(self::LINE_ITEM_ID_SHIPPING);
143
  $shippingLineItem->setQuantity(1);
144
  // If shipping doesn't include handling we should subtract handling from the total shipping amount
145
- $amount = $quoteAddress->getShippingAmount();
146
  if (!$merchantSettings->isShippingIncludeHandling()){
147
- $amount -= $this->getHandlingFeeByMethodName($quoteAddress->getShippingMethod());
148
  }
149
  $shippingLineItem->setGrossAmount($amount);
150
- $this->applyDiscountToLineItem($shippingLineItem,$quoteAddress->getShippingDiscountAmount());
151
  return $shippingLineItem;
152
  }
153
 
 
 
154
  /**
155
  * Returns handling feed amount by given name, or 0 if there is no handling
156
  * @param $name
157
  * @return void
158
  */
159
  private function getHandlingFeeByMethodName($name){
160
- if (strpos($name, "_")) $name = substr($name,0,strpos($name, "_"));
 
161
  // Fetch carriers information from Magento config to determine handling amount
162
  $carriers = Mage::getStoreConfig('carriers');
163
- if (!array_key_exists($name, $carriers)) return 0;
164
  foreach($carriers as $id => $carrier){
165
- if (array_key_exists('handling_fee', $carrier) ){
166
  if ($id == $name)
167
  return $carrier['handling_fee'];
168
  }
169
  }
170
- return 0;
171
  }
172
 
173
- public function getHandlingLineItem(Mage_Sales_Model_Quote_Address $quoteAddress,
174
- Exactor_Core_Model_MerchantSettings $merchantSettings){
175
- if ($quoteAddress->getAddressType() == Mage_Sales_Model_Quote_Address::TYPE_BILLING) return null; // There is no shipping fees there
176
  if ($merchantSettings->isShippingIncludeHandling()) return null; // Handling already included in the shipping
177
  $handlingLineItem = new LineItemType();
178
  $handlingLineItem->setId(self::LINE_ITEM_ID_HANDLING);
179
  $handlingLineItem->setDescription(self::MSG_HANDLING_FEE);
180
  $handlingLineItem->setSKU(self::EUC_HANDLING);
181
  $handlingLineItem->setQuantity(1);
182
- $handlingLineItem->setGrossAmount($this->getHandlingFeeByMethodName($quoteAddress->getShippingMethod()));
183
- if ($handlingLineItem->getGrossAmount()==0) return null;
184
  return $handlingLineItem;
185
  }
186
 
 
 
 
 
 
 
187
  /**
188
  * @param \Mage_Sales_Model_Quote_Address_Item|\Mage_Sales_Model_Quote_Item $magentoItem
189
  * @param Mage_Sales_Model_Quote_Address $quoteAddress
190
  * @param Exactor_Core_Model_MerchantSettings $merchantSettings
191
  * @return LineItemType
192
  */
193
- public function buildLineItemForMagentoItem(Mage_Sales_Model_Quote_Item $magentoItem,
194
  Mage_Sales_Model_Quote_Address $quoteAddress,
195
  Exactor_Core_Model_MerchantSettings $merchantSettings){
196
  $lineItem = new LineItemType();
@@ -199,21 +252,35 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
199
  $lineItem->setGrossAmount($magentoItem->getBaseRowTotal());
200
  else
201
  $lineItem->setGrossAmount(0.0);
 
 
202
  $lineItem->setQuantity($magentoItem->getTotalQty());
203
- $lineItem->setSKU($this->getSKUForItem($magentoItem, $merchantSettings));
 
 
 
 
204
  $this->applyDiscountToLineItem($lineItem, $magentoItem->getDiscountAmount());
 
 
 
 
 
 
 
 
205
  return $lineItem;
206
  }
207
 
208
  public function applyDiscountToLineItem(LineItemType &$item, $discountAmount=0){
209
- if ($discountAmount>0){
210
  $discountedLine = self::MSG_DISCOUNTED_BY . $discountAmount;
211
  $item->setDescription($item->getDescription() . " ($discountedLine)");
212
  $item->setGrossAmount($item->getGrossAmount() - $discountAmount);
213
  }
214
  }
215
 
216
- public function getExemptionIdForQuoteAddress(Mage_Sales_Model_Quote_Address $quoteAddress,
217
  Exactor_Core_Model_MerchantSettings $merchantSettings){
218
  $exemptionId = '';
219
  if ($merchantSettings->getExemptionsSupported()){
@@ -223,34 +290,65 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
223
  return $exemptionId;
224
  }
225
 
 
 
 
 
 
 
 
 
 
 
226
  private function getCurrentCurrencyCode(Mage_Sales_Model_Quote_Address $quoteAddress){
227
  $store = Mage::app()->getStore();
228
  if ($quoteAddress->getQuote() != null && $quoteAddress->getQuote()->getStoreId() != null){
229
  $store = $quoteAddress->getQuote()->getStore();
230
  }
231
- $currency = $store->getBaseCurrencyCode();
232
- if ($currency == null) $currency = 'USD';
 
 
 
 
 
 
233
  return $currency;
234
  }
235
 
 
 
 
 
 
 
 
 
 
236
  /**
237
  * @param Mage_Sales_Model_Quote_Address $quoteAddress
238
  * @param Exactor_Core_Model_MerchantSettings $merchantSettings
239
  * @param bool $isMultishipping
 
240
  * @return InvoiceRequestType
241
  */
242
  public function buildInvoiceRequestForQuoteAddress(Mage_Sales_Model_Quote_Address $quoteAddress,
243
  Exactor_Core_Model_MerchantSettings $merchantSettings,
244
- $isMultishipping){
245
  // Building Invoice Parts
246
  $shipToAddress = $this->buildExactorAddressForQuoteAddress($quoteAddress);
247
  // Trying to find billing address in the quote
248
  $billingAddress = $this->buildExactorAddressForQuoteAddress($quoteAddress->getQuote()->getBillingAddress());
249
- $isEstimation = !$billingAddress->hasData();
250
  if ($isEstimation) $shipToAddress->setFullName(self::MSG_ESTIMATION_REQUEST);
251
  // If this is just tax estimation for not logged in user
252
  // we just need to use shipping as billing
253
  if ($isEstimation){
 
 
 
 
 
254
  $billingAddress = $shipToAddress;
255
  }
256
  // If shipping info unavailable - fallback to billing information
@@ -267,13 +365,14 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
267
  $invoiceRequest->setExemptionId($this->getExemptionIdForQuoteAddress($quoteAddress, $merchantSettings));
268
  // Line items list
269
  $magentoItems = $quoteAddress->getAllItems();
270
- $itemNum = 0;
271
  /**
272
  * @var $magentoItem Mage_Sales_Model_Quote_Item
273
  */
274
  foreach ($magentoItems as $magentoItem){
275
  $exactorLineItem = $this->buildLineItemForMagentoItem($magentoItem, $quoteAddress, $merchantSettings);
276
- $exactorLineItem->setId(self::INDEXED_LINE_ITEM_ID_PREFIX . $magentoItem->getProductId());
 
277
  // If this is non-multishipping request we should set
278
  // ship to address to billing address for VIRTUAL ITEMS
279
  if (!$isMultishipping){
@@ -286,9 +385,92 @@ class Exactor_Tax_Helper_Mapping extends Mage_Core_Helper_Abstract {
286
  }
287
  $invoiceRequest->addLineItem($exactorLineItem);
288
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  // Shipping & Handling
290
- $invoiceRequest->addLineItem($this->getShippingLineItem($quoteAddress, $merchantSettings));
291
- $invoiceRequest->addLineItem($this->getHandlingLineItem($quoteAddress, $merchantSettings));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  return $invoiceRequest;
293
  }
 
294
  }
11
  const EUC_SHIPPING_USPS='EUC-13030202';
12
  const EUC_SHIPPING_AND_HANDLING='EUC-13010101';
13
  const EUC_HANDLING = 'EUC-13010301';
14
+ const EUC_NON_TAXABLE = 'EUC-99990101';
15
+ const EUC_GIFT_CARD = self::EUC_NON_TAXABLE;
16
 
17
  const MSG_DEFAULT_SHIPPING_NAME = 'Default Shipping';
18
  const MSG_HANDLING_FEE = 'Handling Fee';
19
+ const MSG_ADJUSTMENTS = 'Adjustments';
20
  const MSG_SHIPPING_DESCRIPTION_PREFIX = 'Shipping Fee: ';
21
  const MSG_ESTIMATION_REQUEST = 'Magento Tax Estimation Request';
22
  const MSG_DISCOUNTED_BY = 'Discounted by $';
23
+ const MSG_ADJUSTMENTS_REFUND = "Adjustments Refund";
24
+ const MSG_GIFT_CARD_ITEM = "Gift card(-s)";
25
 
26
  const LINE_ITEM_ID_SHIPPING = "SHIPPING";
27
  const LINE_ITEM_ID_HANDLING = "HANDLING";
28
+ const LINE_ITEM_ID_ADJUSTMENTS = "ADJUSTMENTS";
29
  const INDEXED_LINE_ITEM_ID_PREFIX = '_';
30
 
31
  const ATTRIBUTE_NAME_EXEMPTION = 'taxvat';
38
  const PO_ESTIMATE_TEXT = 'Estimated Tax ';
39
 
40
  const UNKNOWN_STREET_TEXT = "";
41
+ const UNKNOWN_STATE_NAME = "UNKNOWN";
42
+ const UNKNOWN_ZIP_CODE = "00000";
43
 
44
 
45
+ const PRICE_TYPE_DYNAMIC = 0;
46
 
47
  private function getLogger(){
48
  if ($this->logger==null)
66
  return join(' ', $parts);
67
  }
68
 
69
+ private function buildExactorAddressForAbstractAddress(Mage_Customer_Model_Address_Abstract $address){
70
  $exactorAddress = new AddressType();
71
  if ($address==null) return null;
72
  // Set defaults
74
  $exactorAddress->setFullName("Unknown Buyer");
75
  //
76
  $fullName = trim($address->getName());//trim($this->buildFullName($address->getFirstname(), $address->getLastname(), $address->getMiddlename()));
77
+ if (strlen($fullName)> self::PRICE_TYPE_DYNAMIC)
78
  $exactorAddress->setFullName($fullName);
79
  if ($address->getStreetFull() != null)
80
  $exactorAddress->setStreet1($address->getStreetFull());
85
  return $exactorAddress;
86
  }
87
 
88
+ public function buildExactorAddressForOrderAddress(Mage_Sales_Model_Order_Address $address){
89
+ return $this->buildExactorAddressForAbstractAddress($address);
90
+ }
91
+
92
+ public function buildExactorAddressForQuoteAddress(Mage_Sales_Model_Quote_Address $address){
93
+ return $this->buildExactorAddressForAbstractAddress($address);
94
+ }
95
+
96
+ /**
97
+ * @param Mage_Sales_Model_Quote_Item_Abstract $magentoItem
98
+ * @param Exactor_Core_Model_MerchantSettings $merchantSettings
99
+ * @return string
100
+ */
101
+ public function getSKUForItem($magentoItem,
102
  Exactor_Core_Model_MerchantSettings $merchantSettings){
103
  $sku='';
104
+ $product = $magentoItem->getProduct();
105
+ if ($product == null)
106
+ $product = Mage::getModel('catalog/product')
107
+ ->setStoreId($magentoItem->getStoreId())
108
+ ->load($magentoItem->getProductId());
109
  switch ($merchantSettings->getSourceOfSKU()){
110
  case Exactor_Core_Model_MerchantSettings::SKU_SOURCE_NONE:
111
  $sku = '';
112
  break;
113
  case Exactor_Core_Model_MerchantSettings::SKU_SOURCE_SKU_FIELD:
114
+ $sku = $magentoItem->getSku();
115
  break;
116
  case Exactor_Core_Model_MerchantSettings::SKU_SOURCE_ATTRIBUTE_NAME:
117
  $attributeSetName = 'Default';
118
  try{
119
  $attributeSetModel = Mage::getModel("eav/entity_attribute_set");
120
+ $attributeSetModel->load($product->getAttributeSetId());
121
  $attributeSetName = $attributeSetModel->getAttributeSetName();
122
  }catch(Exception $e){}
123
  $sku = $attributeSetName;
124
  break;
125
  case Exactor_Core_Model_MerchantSettings::SKU_SOURCE_PRODUCT_CATEGORY:
126
+ $category = $product->getCategory();
127
  if ($category != null)
128
  $sku = $category->getName();
129
  break;
131
  /** @var Mage_Tax_Model_Mysql4_Class_Collection $taxClassCollection */
132
  $taxClassCollection = Mage::getModel('tax/class')->getCollection();
133
  /** @var Mage_Tax_Model_Class $taxClass */
134
+ $taxClass = $taxClassCollection->getItemById($product->getTaxClassId());
135
  if ($taxClass == null) $sku = ''; else $sku = $taxClass->getClassName();
136
  break;
137
  }
138
+ return substr($sku, self::PRICE_TYPE_DYNAMIC, self::MAX_SKU_CODE_LENGTH); // Max length for SKU is 16 characters
139
  }
140
 
141
  private function isUSPSShipping($methodName){
148
  return false;
149
  }
150
 
151
+ public function getShippingLineItemForQuoteAddress(Mage_Sales_Model_Quote_Address $quoteAddress,
152
  Exactor_Core_Model_MerchantSettings $merchantSettings){
153
+
154
  if ($quoteAddress->getAddressType() == Mage_Sales_Model_Quote_Address::TYPE_BILLING) return null; // There is no shipping fees there
155
+ if ($quoteAddress->getShippingAmount()== self::PRICE_TYPE_DYNAMIC) return null;
156
+ $shippingLineItem = $this->getShippingLineItem($merchantSettings, $quoteAddress->getShippingMethod(),
157
+ $quoteAddress->getShippingDescription(),
158
+ $quoteAddress->getShippingAmount(),
159
+ $quoteAddress->getShippingDiscountAmount());
160
+ return $shippingLineItem;
161
+ }
162
+
163
+ public function getShippingLineItemForCreditMemo(Mage_Sales_Model_Order_Creditmemo $creditMemo, Exactor_Core_Model_MerchantSettings $merchantSettings){
164
+ if ($creditMemo->getShippingAmount()== self::PRICE_TYPE_DYNAMIC) return null;
165
+ $lineItem = $this->getShippingLineItem($merchantSettings, $creditMemo->getOrder()->getShippingMethod(),
166
+ $creditMemo->getOrder()->getShippingDescription(),
167
+ $creditMemo->getShippingAmount());
168
+ // For refunds we shouldn't subtract handling amount
169
+ $lineItem->setGrossAmount($creditMemo->getShippingAmount());
170
+ return $lineItem;
171
+ }
172
+
173
+ private function getShippingLineItem(Exactor_Core_Model_MerchantSettings $merchantSettings,
174
+ $carrierName, $carrierDescription, $amount, $discount=0){
175
  $shippingLineItem = new LineItemType();
176
+ $shippingLineItem->setDescription(self::MSG_SHIPPING_DESCRIPTION_PREFIX . $carrierDescription);
177
  if (trim($shippingLineItem->getDescription())==''){
178
  $shippingLineItem->setDescription(self::MSG_DEFAULT_SHIPPING_NAME);
179
+ }
180
  // Get EUC code for shipping
181
  $shippingEUC = self::EUC_SHIPPING_COMMON_CARRIER;
182
  if ($merchantSettings->isShippingIncludeHandling()){
183
  $shippingEUC = self::EUC_SHIPPING_AND_HANDLING;
184
+ }else if ($this->isUSPSShipping($carrierName)){
185
  $shippingEUC = self::EUC_SHIPPING_USPS;
186
  }
187
  $shippingLineItem->setSKU($shippingEUC);
189
  $shippingLineItem->setId(self::LINE_ITEM_ID_SHIPPING);
190
  $shippingLineItem->setQuantity(1);
191
  // If shipping doesn't include handling we should subtract handling from the total shipping amount
 
192
  if (!$merchantSettings->isShippingIncludeHandling()){
193
+ $amount -= $this->getHandlingFeeByMethodName($carrierName);
194
  }
195
  $shippingLineItem->setGrossAmount($amount);
196
+ $this->applyDiscountToLineItem($shippingLineItem,$discount);
197
  return $shippingLineItem;
198
  }
199
 
200
+
201
+
202
  /**
203
  * Returns handling feed amount by given name, or 0 if there is no handling
204
  * @param $name
205
  * @return void
206
  */
207
  private function getHandlingFeeByMethodName($name){
208
+ if (strpos($name, "_")) $name = substr($name, self::PRICE_TYPE_DYNAMIC,strpos($name, "_"));
209
+ if ($name == null) $name="";
210
  // Fetch carriers information from Magento config to determine handling amount
211
  $carriers = Mage::getStoreConfig('carriers');
212
+ if (!array_key_exists($name, $carriers)) return self::PRICE_TYPE_DYNAMIC;
213
  foreach($carriers as $id => $carrier){
214
+ if (array_key_exists('handling_fee', $carrier)){
215
  if ($id == $name)
216
  return $carrier['handling_fee'];
217
  }
218
  }
219
+ return self::PRICE_TYPE_DYNAMIC;
220
  }
221
 
222
+ public function getHandlingLineItem(Exactor_Core_Model_MerchantSettings $merchantSettings, $carrierName){
 
 
223
  if ($merchantSettings->isShippingIncludeHandling()) return null; // Handling already included in the shipping
224
  $handlingLineItem = new LineItemType();
225
  $handlingLineItem->setId(self::LINE_ITEM_ID_HANDLING);
226
  $handlingLineItem->setDescription(self::MSG_HANDLING_FEE);
227
  $handlingLineItem->setSKU(self::EUC_HANDLING);
228
  $handlingLineItem->setQuantity(1);
229
+ $handlingLineItem->setGrossAmount($this->getHandlingFeeByMethodName($carrierName));
230
+ if ($handlingLineItem->getGrossAmount()== self::PRICE_TYPE_DYNAMIC) return null;
231
  return $handlingLineItem;
232
  }
233
 
234
+ private function getHandlingLineItemForQuoteAddress(Mage_Sales_Model_Quote_Address $quoteAddress,
235
+ Exactor_Core_Model_MerchantSettings $merchantSettings){
236
+ if ($quoteAddress->getAddressType() == Mage_Sales_Model_Quote_Address::TYPE_BILLING) return null; // There is no shipping fees there
237
+ return $this->getHandlingLineItem($merchantSettings, $quoteAddress->getShippingMethod());
238
+ }
239
+
240
  /**
241
  * @param \Mage_Sales_Model_Quote_Address_Item|\Mage_Sales_Model_Quote_Item $magentoItem
242
  * @param Mage_Sales_Model_Quote_Address $quoteAddress
243
  * @param Exactor_Core_Model_MerchantSettings $merchantSettings
244
  * @return LineItemType
245
  */
246
+ public function buildLineItemForMagentoItem($magentoItem,
247
  Mage_Sales_Model_Quote_Address $quoteAddress,
248
  Exactor_Core_Model_MerchantSettings $merchantSettings){
249
  $lineItem = new LineItemType();
252
  $lineItem->setGrossAmount($magentoItem->getBaseRowTotal());
253
  else
254
  $lineItem->setGrossAmount(0.0);
255
+ // We should exclude 0 amount items
256
+ if ($lineItem->getGrossAmount() == self::PRICE_TYPE_DYNAMIC) return null;
257
  $lineItem->setQuantity($magentoItem->getTotalQty());
258
+ if ($magentoItem instanceof Mage_Sales_Model_Order_Creditmemo_Item){
259
+ $lineItem->setSKU($this->getSKUForItem($magentoItem->getOrderItem(), $merchantSettings));
260
+ }else{
261
+ $lineItem->setSKU($this->getSKUForItem($magentoItem, $merchantSettings));
262
+ }
263
  $this->applyDiscountToLineItem($lineItem, $magentoItem->getDiscountAmount());
264
+ // Check if it is bundle or groped product
265
+ if (in_array($magentoItem->getProductType(), array(Mage_Catalog_Model_Product_Type::TYPE_BUNDLE))) {
266
+ if ($magentoItem->getProduct()->getPriceType() == self::PRICE_TYPE_DYNAMIC) {
267
+ $lineItem->setGrossAmount(self::PRICE_TYPE_DYNAMIC);
268
+ $lineItem->setDescription($lineItem->getDescription() . " :: Dynamic Price");
269
+ return null; // Doesn't show it in the Exactor transaction.
270
+ }
271
+ }
272
  return $lineItem;
273
  }
274
 
275
  public function applyDiscountToLineItem(LineItemType &$item, $discountAmount=0){
276
+ if ($discountAmount> self::PRICE_TYPE_DYNAMIC){
277
  $discountedLine = self::MSG_DISCOUNTED_BY . $discountAmount;
278
  $item->setDescription($item->getDescription() . " ($discountedLine)");
279
  $item->setGrossAmount($item->getGrossAmount() - $discountAmount);
280
  }
281
  }
282
 
283
+ private function getExemptionIdForQuoteAddress(Mage_Sales_Model_Quote_Address $quoteAddress,
284
  Exactor_Core_Model_MerchantSettings $merchantSettings){
285
  $exemptionId = '';
286
  if ($merchantSettings->getExemptionsSupported()){
290
  return $exemptionId;
291
  }
292
 
293
+ private function getExemptionIdForCreditMemo(Mage_Sales_Model_Order_Creditmemo $creditMemo,
294
+ Exactor_Core_Model_MerchantSettings $merchantSettings){
295
+ $exemptionId = '';
296
+ if ($merchantSettings->getExemptionsSupported()){
297
+ $customerExemptionId = $creditMemo->getOrder()->getCustomerTaxvat();
298
+ if ($customerExemptionId!=null) $exemptionId=$customerExemptionId;
299
+ }
300
+ return $exemptionId;
301
+ }
302
+
303
  private function getCurrentCurrencyCode(Mage_Sales_Model_Quote_Address $quoteAddress){
304
  $store = Mage::app()->getStore();
305
  if ($quoteAddress->getQuote() != null && $quoteAddress->getQuote()->getStoreId() != null){
306
  $store = $quoteAddress->getQuote()->getStore();
307
  }
308
+ return $this->getCurrencyCodeForStore($store);
309
+ }
310
+
311
+ private function getCurrencyCodeForStore(Mage_Core_Model_Store $store){
312
+ $currency = 'USD';
313
+ if ($store != null){
314
+ $currency = $store->getBaseCurrencyCode()!=null ? $store->getBaseCurrencyCode() : $currency;
315
+ }
316
  return $currency;
317
  }
318
 
319
+ private function buildGiftCardLineItem($amount){
320
+ $lineItem = new LineItemType();
321
+ $lineItem->setGrossAmount($amount);
322
+ $lineItem->setQuantity(1);
323
+ $lineItem->setSKU(self::EUC_GIFT_CARD);
324
+ $lineItem->setDescription(self::MSG_GIFT_CARD_ITEM);
325
+ return $lineItem;
326
+ }
327
+
328
  /**
329
  * @param Mage_Sales_Model_Quote_Address $quoteAddress
330
  * @param Exactor_Core_Model_MerchantSettings $merchantSettings
331
  * @param bool $isMultishipping
332
+ * @param $isEstimation
333
  * @return InvoiceRequestType
334
  */
335
  public function buildInvoiceRequestForQuoteAddress(Mage_Sales_Model_Quote_Address $quoteAddress,
336
  Exactor_Core_Model_MerchantSettings $merchantSettings,
337
+ $isMultishipping, $isEstimation){
338
  // Building Invoice Parts
339
  $shipToAddress = $this->buildExactorAddressForQuoteAddress($quoteAddress);
340
  // Trying to find billing address in the quote
341
  $billingAddress = $this->buildExactorAddressForQuoteAddress($quoteAddress->getQuote()->getBillingAddress());
342
+
343
  if ($isEstimation) $shipToAddress->setFullName(self::MSG_ESTIMATION_REQUEST);
344
  // If this is just tax estimation for not logged in user
345
  // we just need to use shipping as billing
346
  if ($isEstimation){
347
+ // It is possible that postal code or state will be missing on the tax estimation form
348
+ // In this case we will try to determine region basing on the given Postal Code
349
+ if (strlen(trim($shipToAddress->getStateOrProvince()))== self::PRICE_TYPE_DYNAMIC && strlen(trim($shipToAddress->getPostalCode()))!= self::PRICE_TYPE_DYNAMIC){
350
+ $shipToAddress->setStateOrProvince(RegionResolver::getInstance()->getStateOrProvinceByCode(trim($shipToAddress->getPostalCode())));
351
+ }
352
  $billingAddress = $shipToAddress;
353
  }
354
  // If shipping info unavailable - fallback to billing information
365
  $invoiceRequest->setExemptionId($this->getExemptionIdForQuoteAddress($quoteAddress, $merchantSettings));
366
  // Line items list
367
  $magentoItems = $quoteAddress->getAllItems();
368
+ $itemNum = self::PRICE_TYPE_DYNAMIC;
369
  /**
370
  * @var $magentoItem Mage_Sales_Model_Quote_Item
371
  */
372
  foreach ($magentoItems as $magentoItem){
373
  $exactorLineItem = $this->buildLineItemForMagentoItem($magentoItem, $quoteAddress, $merchantSettings);
374
+ if ($exactorLineItem == null) continue;
375
+ $exactorLineItem->setId(self::INDEXED_LINE_ITEM_ID_PREFIX . $magentoItem->getId());
376
  // If this is non-multishipping request we should set
377
  // ship to address to billing address for VIRTUAL ITEMS
378
  if (!$isMultishipping){
385
  }
386
  $invoiceRequest->addLineItem($exactorLineItem);
387
  }
388
+ // Gift Cards
389
+ if ($quoteAddress->getBaseGiftCardsAmount() != null && $quoteAddress->getBaseGiftCardsAmount() != self::PRICE_TYPE_DYNAMIC){
390
+ $invoiceRequest->addLineItem($this->buildGiftCardLineItem(-1 * $quoteAddress->getBaseGiftCardsAmount()));
391
+ }
392
+ // Shipping & Handling
393
+ $invoiceRequest->addLineItem($this->getShippingLineItemForQuoteAddress($quoteAddress, $merchantSettings));
394
+ $invoiceRequest->addLineItem($this->getHandlingLineItemForQuoteAddress($quoteAddress, $merchantSettings));
395
+ return $invoiceRequest;
396
+ }
397
+
398
+
399
+
400
+ /**Return invoice requests by credit memo. In case if credit memo contains adjustments - returns 2 separated invoices
401
+ * @param Mage_Sales_Model_Order_Creditmemo $creditMemo
402
+ * @param Exactor_Core_Model_MerchantSettings $merchantSettings
403
+ * @return array InvoiceRequestType
404
+ */
405
+ public function buildInvoiceRequestsForCreditMemo(Mage_Sales_Model_Order_Creditmemo $creditMemo, Exactor_Core_Model_MerchantSettings $merchantSettings){
406
+ $result = array();
407
+ $invoiceRequest = new InvoiceRequestType();
408
+ $invoiceRequest->setBillTo($this->buildExactorAddressForOrderAddress($creditMemo->getBillingAddress()));
409
+ $invoiceRequest->setShipTo($this->buildExactorAddressForOrderAddress($creditMemo->getShippingAddress()));
410
+ $invoiceRequest->setShipFrom($merchantSettings->getExactorShippingAddress());
411
+ $invoiceRequest->setCurrencyCode($creditMemo->getOrder()->getOrderCurrencyCode());
412
+ $invoiceRequest->setPurchaseOrderNumber("Refund");
413
+ $invoiceRequest->setSaleDate(new DateTime("@".$creditMemo->getOrder()->getCreatedAtDate()->getTimestamp()));
414
+ $invoiceRequest->setExemptionId($this->getExemptionIdForCreditMemo($creditMemo, $merchantSettings));
415
+ // Line items
416
+ $magentoItems = $creditMemo->getAllItems();
417
+ foreach ($magentoItems as $magentoItem){
418
+ $exactorLineItem = $this->buildLineItemForMagentoItem($magentoItem, new Mage_Sales_Model_Quote_Address(), $merchantSettings);
419
+ if ($exactorLineItem != null){
420
+ $exactorLineItem->setQuantity($magentoItem->getQty());
421
+ }
422
+ $invoiceRequest->addLineItem($exactorLineItem);
423
+ }
424
  // Shipping & Handling
425
+ $shippingLineItem = $this->getShippingLineItemForCreditMemo($creditMemo, $merchantSettings);
426
+ $handlingLineItem = null;
427
+
428
+ if ($shippingLineItem!=null) {
429
+ $handlingLineItem = $this->getHandlingLineItem($merchantSettings, $creditMemo->getOrder()->getShippingMethod());
430
+ if ($handlingLineItem!=null){
431
+ $delta = $creditMemo->getOrder()->getBaseShippingAmount() - $creditMemo->getOrder()->getBaseShippingRefunded() + $creditMemo->getBaseShippingAmount();
432
+ $shippingFactor = $creditMemo->getBaseShippingAmount() / $delta;
433
+ $handlingAmount = $handlingLineItem->getGrossAmount()*$shippingFactor;
434
+ $shippingAmount = $creditMemo->getBaseShippingAmount() - $handlingAmount;
435
+ // Update amounts
436
+ $shippingLineItem->setGrossAmount($creditMemo->getStore()->roundPrice($shippingAmount));
437
+ $handlingLineItem->setGrossAmount($creditMemo->getStore()->roundPrice($handlingAmount));
438
+ }
439
+ }
440
+
441
+ $invoiceRequest->addLineItem($shippingLineItem);
442
+ $invoiceRequest->addLineItem($handlingLineItem);
443
+
444
+ $result[] = $invoiceRequest;
445
+ // Adjustments
446
+ $adjustmentsItem = new LineItemType();
447
+ $adjustmentsItem->setId(self::LINE_ITEM_ID_ADJUSTMENTS);
448
+ $adjustmentsItem->setDescription(self::MSG_ADJUSTMENTS);
449
+ $adjustmentsItem->setQuantity(1);
450
+ $adjustmentsItem->setSKU(self::EUC_NON_TAXABLE);
451
+ $adjustmentsItem->setGrossAmount($creditMemo->getAdjustmentPositive()-$creditMemo->getAdjustmentNegative());
452
+ if ($adjustmentsItem->getGrossAmount() != self::PRICE_TYPE_DYNAMIC ||
453
+ $creditMemo->getBaseGiftCardsAmount() != null && $creditMemo->getBaseGiftCardsAmount() != self::PRICE_TYPE_DYNAMIC
454
+ ){
455
+ $adjustmentInvoice = clone $invoiceRequest;
456
+ $adjustmentInvoice->setPurchaseOrderNumber(self::MSG_ADJUSTMENTS_REFUND);
457
+ $adjustmentInvoice->setLineItems(array());
458
+ if ($adjustmentsItem->getGrossAmount() != self::PRICE_TYPE_DYNAMIC){
459
+ $adjustmentInvoice->addLineItem($adjustmentsItem);
460
+ }
461
+ // Gift Cards
462
+ if ($creditMemo->getBaseGiftCardsAmount() != null && $creditMemo->getBaseGiftCardsAmount() != self::PRICE_TYPE_DYNAMIC){
463
+ $adjustmentInvoice->addLineItem($this->buildGiftCardLineItem(-1 * $creditMemo->getBaseGiftCardsAmount()));
464
+ }
465
+ $result[] = $adjustmentInvoice;
466
+ }
467
+ return $result;
468
+ }
469
+
470
+ public function buildInvoiceRequestForMagentoInvoice(){
471
+ $invoiceRequest = new InvoiceRequestType();
472
+
473
  return $invoiceRequest;
474
  }
475
+
476
  }
app/code/local/Exactor/Tax/Model/Sales/Total/Quote/Tax.php CHANGED
@@ -61,6 +61,7 @@ class Exactor_Tax_Model_Sales_Total_Quote_Tax extends Mage_Sales_Model_Quote_Add
61
  require_once($libDir . '/XmlProcessing.php');
62
  require_once($libDir . '/ExactorDomainObjects.php');
63
  require_once($libDir . '/ExactorCommons.php');
 
64
  // Magento specific definitions
65
  require_once($libDir . '/Magento.php');
66
  require_once($libDir . '/config.php');
@@ -128,7 +129,7 @@ class Exactor_Tax_Model_Sales_Total_Quote_Tax extends Mage_Sales_Model_Quote_Add
128
  //return $this->processTaxCalculationFail('Missing or invalid Merchant Settings');
129
  }
130
  // Preparing Exactor Invoice Request
131
- $invoiceRequest = $this->exactorMappingHelper->buildInvoiceRequestForQuoteAddress($address, $merchantSettings, $this->isMultishippingRequest());
132
  $this->logger->trace('Invoice ' . serialize($invoiceRequest),'collect');
133
  if ($invoiceRequest != null && $this->checkIfCalculationNeeded($invoiceRequest, $merchantSettings)){
134
  // Sending to Exactor Tax Calculation Request to Exactor
@@ -170,6 +171,7 @@ class Exactor_Tax_Model_Sales_Total_Quote_Tax extends Mage_Sales_Model_Quote_Add
170
  foreach ($address->getAllItems() as $item){
171
  $shippingTax -= $item->getTaxAmount();
172
  }
 
173
  // The following code is workaraund for the bug in Magento 1.6.2 - Tax applied to the QuoteAddress object
174
  // can be missed by Magento for some reason. This issue can be reproduced in very trickily manner:
175
  // 1. login
@@ -193,6 +195,16 @@ class Exactor_Tax_Model_Sales_Total_Quote_Tax extends Mage_Sales_Model_Quote_Add
193
  }
194
 
195
 
 
 
 
 
 
 
 
 
 
 
196
  /**
197
  * Set tax amount for each item
198
  * @param \Mage_Sales_Model_Quote_Address $address
@@ -205,7 +217,7 @@ class Exactor_Tax_Model_Sales_Total_Quote_Tax extends Mage_Sales_Model_Quote_Add
205
  * @var Mage_Sales_Model_Quote_Item $item
206
  */
207
  foreach ($address->getAllItems() as $item){
208
- $taxResultItem = $invoice->getItemById(Exactor_Tax_Helper_Mapping::INDEXED_LINE_ITEM_ID_PREFIX . $item->getProductId());
209
  // If there is no item in response - set tax to 0
210
  if ($taxResultItem==null){
211
  $item->setTaxAmount(0);
@@ -249,6 +261,7 @@ class Exactor_Tax_Model_Sales_Total_Quote_Tax extends Mage_Sales_Model_Quote_Add
249
  */
250
  private function checkIfCalculationNeeded($invoiceRequest, $merchantSettings){
251
  // Calculating digital signature for the current request
 
252
  $taxRequest = ExactorConnectionFactory::getInstance()->buildRequest($merchantSettings->getMerchantID(), $merchantSettings->getUserID());
253
  $taxRequest->addInvoiceRequest($invoiceRequest);
254
  $signatureBuilder = new ExactorDigitalSignatureBuilder();
61
  require_once($libDir . '/XmlProcessing.php');
62
  require_once($libDir . '/ExactorDomainObjects.php');
63
  require_once($libDir . '/ExactorCommons.php');
64
+ require_once($libDir . '/RegionResolver.php');
65
  // Magento specific definitions
66
  require_once($libDir . '/Magento.php');
67
  require_once($libDir . '/config.php');
129
  //return $this->processTaxCalculationFail('Missing or invalid Merchant Settings');
130
  }
131
  // Preparing Exactor Invoice Request
132
+ $invoiceRequest = $this->exactorMappingHelper->buildInvoiceRequestForQuoteAddress($address, $merchantSettings, $this->isMultishippingRequest(), $this->isEstimation());
133
  $this->logger->trace('Invoice ' . serialize($invoiceRequest),'collect');
134
  if ($invoiceRequest != null && $this->checkIfCalculationNeeded($invoiceRequest, $merchantSettings)){
135
  // Sending to Exactor Tax Calculation Request to Exactor
171
  foreach ($address->getAllItems() as $item){
172
  $shippingTax -= $item->getTaxAmount();
173
  }
174
+ $shippingTax = Mage::app()->getStore()->roundPrice($shippingTax);
175
  // The following code is workaraund for the bug in Magento 1.6.2 - Tax applied to the QuoteAddress object
176
  // can be missed by Magento for some reason. This issue can be reproduced in very trickily manner:
177
  // 1. login
195
  }
196
 
197
 
198
+ /**
199
+ * Returns true is request is just tax estimation
200
+ * @return bool
201
+ */
202
+ private function isEstimation()
203
+ {
204
+ return strpos($this->session->getLastUrl(), "estimatePost") != false
205
+ || strpos($this->session->getLastUrl(),"estimateUpdatePost") != false;
206
+ }
207
+
208
  /**
209
  * Set tax amount for each item
210
  * @param \Mage_Sales_Model_Quote_Address $address
217
  * @var Mage_Sales_Model_Quote_Item $item
218
  */
219
  foreach ($address->getAllItems() as $item){
220
+ $taxResultItem = $invoice->getItemById(Exactor_Tax_Helper_Mapping::INDEXED_LINE_ITEM_ID_PREFIX . $item->getId());
221
  // If there is no item in response - set tax to 0
222
  if ($taxResultItem==null){
223
  $item->setTaxAmount(0);
261
  */
262
  private function checkIfCalculationNeeded($invoiceRequest, $merchantSettings){
263
  // Calculating digital signature for the current request
264
+ if ($invoiceRequest == null || $invoiceRequest->getLineItems() == null) return false;
265
  $taxRequest = ExactorConnectionFactory::getInstance()->buildRequest($merchantSettings->getMerchantID(), $merchantSettings->getUserID());
266
  $taxRequest->addInvoiceRequest($invoiceRequest);
267
  $signatureBuilder = new ExactorDigitalSignatureBuilder();
app/code/local/Exactor/Tax/etc/config.xml CHANGED
@@ -28,7 +28,7 @@
28
  <config>
29
  <modules>
30
  <Exactor_Tax>
31
- <version>2012.06.14</version>
32
  </Exactor_Tax>
33
  </modules>
34
  <global>
28
  <config>
29
  <modules>
30
  <Exactor_Tax>
31
+ <version>2012.09.20</version>
32
  </Exactor_Tax>
33
  </modules>
34
  <global>
app/design/adminhtml/default/default/template/ExactorSettings/settingsform.phtml CHANGED
@@ -35,6 +35,16 @@
35
  if ($accountSettings->getID() == null){
36
  $accountSettings->setStoreViewID($this->getStoreViewId());
37
  }
 
 
 
 
 
 
 
 
 
 
38
  ?>
39
  <div class="content-header">
40
  <table cellspacing="0" class="grid-header">
@@ -105,6 +115,19 @@
105
  <td class="label"><?php echo $this->__('City')?> <span class="required">*</span></td>
106
  <td class="input-ele"><input class="input-text validate-length required-entry" maxlength="128" name="exactordetailsform[City]" value="<?php echo $accountSettings->getCity();?>"/></td>
107
  </tr>
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  <tr>
109
  <td class="label"><?php echo $this->__('State Or Province')?> <span class="required">*</span></td>
110
  <td class="input-ele">
@@ -113,30 +136,17 @@
113
  </select>
114
  <script type="text/javascript">
115
  //<![CDATA[
116
- $('StateOrProvince_Id').setAttribute('defaultValue', "<?php echo $accountSettings->getStateOrProvince() ?>");
117
  //]]>
118
  </script>
119
  <input id="StateOrProvince" class="input-text validate-length"
120
  value="<?php echo $accountSettings->getStateOrProvince() ?>"
121
  maxlength="128" name="exactordetailsform[StateOrProvince]" />
122
  </td>
123
- </tr>
124
- <tr>
125
- <td class="label"><?php echo $this->__('Postal Code')?> <span class="required">*</span></td>
126
- <td class="input-ele"><input class="input-text validate-zip required-entry" name="exactordetailsform[PostalCode]" value="<?php echo $accountSettings->getPostalCode();?>"/></td>
127
  </tr>
128
  <tr>
129
- <td class="label"><?php echo $this->__('Country')?> <span class="required">*</span></td>
130
- <td class="input-ele">
131
- <select id="Country" class="required-entry" name="exactordetailsform[Country]" STYLE="width: 250px">
132
- <?php foreach ($country_lists as $country): ?>
133
- <option value=<?php echo $country["value"] ?>
134
- <?php if(($country["value"]==$accountSettings->getCountry()) || (trim($country["value"])=="US" && trim($accountSettings->getCountry())=="")): ?>
135
- selected="true"<?php endif ?>><?php echo $country["label"]?>
136
- </option>
137
- <?php endforeach ?>
138
- </select>
139
- </td>
140
  </tr>
141
  </table>
142
  </fieldset>
@@ -208,7 +218,7 @@
208
  </div>
209
 
210
  <script type="text/javascript">
211
- new RegionUpdater('Country', 'StateOrProvince', 'StateOrProvince_Id',
212
  <?php echo $this->helper('directory')->getRegionJson() ?>);
213
 
214
  var editForm = new varienForm('edit_form');
35
  if ($accountSettings->getID() == null){
36
  $accountSettings->setStoreViewID($this->getStoreViewId());
37
  }
38
+ $stateId = $regionInfo = Mage::getModel('directory/region')->getCollection()
39
+ ->addFilter("code", $accountSettings->getStateOrProvince())
40
+ ->addFilter("country_id", $accountSettings->getCountry())
41
+ ->getFirstItem();
42
+ if ($stateId->hasData()) {
43
+ $stateId = $stateId->getRegionId();
44
+ } else {
45
+ $stateId = 0;
46
+ }
47
+
48
  ?>
49
  <div class="content-header">
50
  <table cellspacing="0" class="grid-header">
115
  <td class="label"><?php echo $this->__('City')?> <span class="required">*</span></td>
116
  <td class="input-ele"><input class="input-text validate-length required-entry" maxlength="128" name="exactordetailsform[City]" value="<?php echo $accountSettings->getCity();?>"/></td>
117
  </tr>
118
+ <tr>
119
+ <td class="label"><?php echo $this->__('Country')?> <span class="required">*</span></td>
120
+ <td class="input-ele">
121
+ <select id="country" class="required-entry" name="exactordetailsform[Country]" STYLE="width: 250px">
122
+ <?php foreach ($country_lists as $country): ?>
123
+ <option value=<?php echo $country["value"] ?>
124
+ <?php if(($country["value"]==$accountSettings->getCountry()) || (trim($country["value"])=="US" && trim($accountSettings->getCountry())=="")): ?>
125
+ selected="true"<?php endif ?>><?php echo $country["label"]?>
126
+ </option>
127
+ <?php endforeach ?>
128
+ </select>
129
+ </td>
130
+ </tr>
131
  <tr>
132
  <td class="label"><?php echo $this->__('State Or Province')?> <span class="required">*</span></td>
133
  <td class="input-ele">
136
  </select>
137
  <script type="text/javascript">
138
  //<![CDATA[
139
+ $('StateOrProvince_Id').setAttribute('defaultValue', "<?php echo $stateId ?>");
140
  //]]>
141
  </script>
142
  <input id="StateOrProvince" class="input-text validate-length"
143
  value="<?php echo $accountSettings->getStateOrProvince() ?>"
144
  maxlength="128" name="exactordetailsform[StateOrProvince]" />
145
  </td>
 
 
 
 
146
  </tr>
147
  <tr>
148
+ <td class="label"><?php echo $this->__('Postal Code')?> <span class="required">*</span></td>
149
+ <td class="input-ele"><input class="input-text validate-zip-international required-entry" name="exactordetailsform[PostalCode]" value="<?php echo $accountSettings->getPostalCode();?>"/></td>
 
 
 
 
 
 
 
 
 
150
  </tr>
151
  </table>
152
  </fieldset>
218
  </div>
219
 
220
  <script type="text/javascript">
221
+ new RegionUpdater('country', 'StateOrProvince', 'StateOrProvince_Id',
222
  <?php echo $this->helper('directory')->getRegionJson() ?>);
223
 
224
  var editForm = new varienForm('edit_form');
lib/ExactorCommons/ExactorCommons.php CHANGED
@@ -428,6 +428,7 @@ class ExactorDigitalSignatureBuilder{
428
  $this->appendValue($invoice->getSaleDate());
429
  $this->appendAddressFields($invoice->getShipTo());
430
  $this->appendAddressFields($invoice->getShipFrom());
 
431
  foreach ($invoice->getLineItems() as $lineItem){
432
  $this->appendValue($lineItem->getGrossAmount());
433
  }
@@ -655,6 +656,46 @@ class ExactorProcessingService{
655
  return $response;
656
  }
657
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  /**
659
  * Fetch information about transaction from the internal database by given internal id
660
  * and send refund request.
428
  $this->appendValue($invoice->getSaleDate());
429
  $this->appendAddressFields($invoice->getShipTo());
430
  $this->appendAddressFields($invoice->getShipFrom());
431
+ if ($invoice->getLineItems() == null) return;
432
  foreach ($invoice->getLineItems() as $lineItem){
433
  $this->appendValue($lineItem->getGrossAmount());
434
  }
656
  return $response;
657
  }
658
 
659
+ /**
660
+ * Performs partial refund basing on the InvoiceRequest information.
661
+ * Basically it just negate all amounts in the given invoice transaction and commits it.
662
+ * @param InvoiceRequestType $invoiceRequest
663
+ * @param DateTime $date
664
+ * @param string $invoiceNumber
665
+ * @return TaxResponseType
666
+ */
667
+ public function partialRefund(InvoiceRequestType $invoiceRequest, DateTime $date, $invoiceNumber="unknown"){
668
+ $commitRequest = new CommitRequestType();
669
+ $commitRequest->setCommitDate($date);
670
+ $commitRequest->setInvoiceNumber($invoiceNumber);
671
+ // Negate all item amounts
672
+ foreach ($invoiceRequest->getLineItems() as $lineItem){
673
+ $lineItem->setGrossAmount(-1 * $lineItem->getGrossAmount());
674
+ }
675
+ $commitRequest->setInvoiceRequest($invoiceRequest);
676
+ $request = ExactorConnectionFactory::getInstance()->buildRequest($this->merchantId, $this->userId, $this->partnerId);
677
+ $request->addCommitRequest($commitRequest);
678
+ $response = ExactorConnectionFactory::getInstance()->buildExactorConnector()->sendRequest($request);
679
+ $signature='';
680
+ if ($response->hasErrors()){
681
+ if ($response->getFirstError()->getErrorCode() == ErrorResponseType::ERROR_INVALID_COMMIT_DATE){
682
+ $commitRequest->setCommitDate(new DateTime());
683
+ $response = ExactorConnectionFactory::getInstance()->buildExactorConnector()->sendRequest($request);
684
+ if ($response->hasErrors()){
685
+ $this->logger->error('Exactor Commit request failed. See debug info for details');
686
+ $this->pluginCallback->onCommitFail($signature, $response->getFirstError(), $commitRequest);
687
+ }
688
+ }else{
689
+ $this->logger->error('Exactor Commit request failed. See debug info for details');
690
+ $this->pluginCallback->onCommitFail($signature, $response->getFirstError(), $commitRequest);
691
+ }
692
+ }else{
693
+ $commitResponses = $response->getCommitResponses();
694
+ $this->pluginCallback->onCommitSuccess($signature, $commitResponses[0], $commitRequest, null);
695
+ }
696
+ return $response;
697
+ }
698
+
699
  /**
700
  * Fetch information about transaction from the internal database by given internal id
701
  * and send refund request.
lib/ExactorCommons/ExactorDomainObjects.php CHANGED
@@ -205,7 +205,7 @@ class AddressType extends XmlSerializationSupport {
205
  */
206
  public function hasData(){
207
  return //strlen(trim($this->getStreet1()))
208
- // && strlen(trim($this->getCity()))
209
  strlen(trim($this->getPostalCode()))
210
  && strlen(trim($this->getStateOrProvince()));
211
  }
205
  */
206
  public function hasData(){
207
  return //strlen(trim($this->getStreet1()))
208
+ // && strlen(trim($this->getCity()))
209
  strlen(trim($this->getPostalCode()))
210
  && strlen(trim($this->getStateOrProvince()));
211
  }
lib/ExactorCommons/RegionResolver.php ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * User: Dmitry Berezovsky
4
+ * Date: 7/31/12
5
+ * Time: 11:37 AM
6
+ */
7
+
8
+ /**
9
+ * This helper class provides methods for determining region name or abbreviation by given postal code.
10
+ * Supports both of US and Canada postal codes.
11
+ */
12
+ class RegionResolver {
13
+ const REGEX_VALID_ZIP = '/^\d{5}(-\d{4})?$/i';
14
+ // Each array contains 3 elements:
15
+ // C_USA_STATE_ABBREVIATION(0) - State abbreviation
16
+ // C_USA_ZIP_LOWER_BOUND(1) - Lower bound of the zip range
17
+ // C_USA_ZIP_UPPER_BOUND(2) - Upper bound of the zip range
18
+ const C_USA_STATE_ABBREVIATION = 0;
19
+ const C_USA_ZIP_LOWER_BOUND = 1;
20
+ const C_USA_ZIP_UPPER_BOUND = 2;
21
+ private $USA_CODES_TABLE = array(array("AL", 35004, 36925), array("AK", 99501, 99950), array("AZ", 85001, 86556),
22
+ array("AR", 71601, 72959), array("CA", 90001, 96162), array("CO", 80001, 81658),
23
+ array("CT", 6001, 6389), array("CT", 6401, 6928), array("DE", 19701, 19980),
24
+ array("DC", 20001, 20098), array("DC", 20201, 20586), array("DC", 20590, 20597),
25
+ array("DC", 20599, 20599), array("DC", 56901, 56972), array("FL", 32003, 33994),
26
+ array("FL", 34101, 34997), array("GA", 30002, 31999), array("GA", 39813, 39901),
27
+ array("HI", 96701, 96797), array("HI", 96801, 96898), array("ID", 83201, 83406),
28
+ array("ID", 83415, 83877), array("IL", 60001, 62999), array("IN", 46001, 47997),
29
+ array("IA", 50001, 52809), array("KS", 66002, 67954), array("KY", 40003, 42788),
30
+ array("LA", 70001, 71497), array("ME", 3901, 4992), array("MD", 20588, 20588),
31
+ array("MD", 20601, 21930), array("MA", 1001, 2791), array("MA", 5501, 5544),
32
+ array("MI", 48001, 49971), array("MN", 55001, 56763), array("MS", 38601, 39776),
33
+ array("MO", 63001, 65899), array("MT", 59001, 59937), array("NE", 68001, 69367),
34
+ array("NV", 88901, 89883), array("NH", 3031, 3897), array("NJ", 7001, 8989),
35
+ array("NM", 87001, 88439), array("NY", 501, 544), array("NY", 6390, 6390),
36
+ array("NY", 10001, 14925), array("NC", 27006, 28909), array("ND", 58001, 58856),
37
+ array("OH", 43001, 45999), array("OK", 73001, 73198), array("OK", 73401, 74966),
38
+ array("OR", 97001, 97920), array("PA", 15001, 19612), array("RI", 2801, 2940),
39
+ array("SC", 29001, 29945), array("SD", 57001, 57799), array("TN", 37010, 38589),
40
+ array("TX", 73301, 73344), array("TX", 75001, 79999), array("TX", 88510, 88595),
41
+ array("UT", 84001, 84791), array("VT", 5001, 5495), array("VT", 5601, 5907),
42
+ array("VA", 20101, 20198), array("VA", 20598, 20598), array("VA", 22003, 24658),
43
+ array("WA", 98001, 99403), array("WV", 24701, 26886), array("WI", 53001, 54990),
44
+ array("WY", 82001, 83128), array("WY", 83414, 83414), array("AS", 96799, 96799),
45
+ array("GU", 96910, 96932), array("MP", 96950, 96952), array("PR", 601, 795),
46
+ array("PR", 901, 988), array("VI", 801, 851), array("FM", 96941, 96944),
47
+ array("MH", 96960, 96970), array("PW", 96939, 96940), array("AA", 34002, 34099),
48
+ array("AE", 9002, 9898), array("AP", 96201, 96698));
49
+ const REGEX_VALID_CA_CODE = '/^[ABCEGHJKLMNPRSTVXY]{1}\d{1}[A-Z]{1} *\d{1}[A-Z]{1}\d{1}$/i';
50
+ // Each array contains 3 elements:
51
+ // C_CAN_REGION_ABBREVIATION(0) - Region abbreviation
52
+ // C_CAN_DISTRICT_IDS(1) - Array of the post district identifiers
53
+ // C_CAN_REGION_NAME(2) - Full region name
54
+ const C_CAN_REGION_ABBREVIATION = 0;
55
+ const C_CAN_DISTRICT_IDS = 1;
56
+ const C_CAN_REGION_NAME = 2;
57
+ private $CANADA_CODES_TABLE = array(array("NL",array("A"),"Newfoundland and Labrador"),
58
+ array("NS",array("B"),"Nova Scotia"),
59
+ array("PE",array("C"),"Prince Edward Island"),
60
+ array("NB",array("E"),"New Brunswick"),
61
+ array("QC",array("G","H","J"),"Quebec"),
62
+ array("ON",array("K","L","M","N","P"),"Ontario"),
63
+ array("MB",array("R"),"Manitoba"),
64
+ array("SK",array("S"),"Saskatchewan"),
65
+ array("AB",array("T"),"Alberta"),
66
+ array("BC",array("V"),"British Columbia"),
67
+ array("NT",array("X"),"Northwest Territories"),
68
+ array("YT",array("Y"),"Yukon")
69
+ );
70
+
71
+ protected static $instance;
72
+ static function __cmp_sort_zip_codes($a, $b){
73
+ if ($a[1] == $b[1]) return 0;
74
+ return ($a[1]>$b[1]) ? 1 : -1;
75
+ }
76
+ private function __construct(){
77
+ usort($this->USA_CODES_TABLE, "RegionResolver::__cmp_sort_zip_codes");
78
+ }
79
+ private function __clone() { }
80
+ private function __wakeup() { }
81
+ /**
82
+ * @static
83
+ * @return RegionResolver
84
+ */
85
+ public static function getInstance() {
86
+ if ( is_null(self::$instance) ) {
87
+ self::$instance = new RegionResolver;
88
+ }
89
+ return self::$instance;
90
+ }
91
+
92
+ /**
93
+ * Returns true if given code is valid USA ZIP code(e.g "94105-0011" or "94105")
94
+ * @param $zip
95
+ * @return bool
96
+ */
97
+ public function isValidUSAZip($zip){
98
+ return preg_match(self::REGEX_VALID_ZIP, $zip)>0;
99
+ }
100
+
101
+ /**
102
+ * Returns true if given code is valid Canadian postal code("T2X 1V4" or "T2X1V4")
103
+ * @param $postalCode
104
+ * @return bool
105
+ */
106
+ public function isValidCanadianPostalCode($postalCode){
107
+ return preg_match(self::REGEX_VALID_CA_CODE, $postalCode)>0;
108
+ }
109
+
110
+ private function find_in_array($array, $callback, $param){
111
+ $res = array();
112
+ foreach($array as $x){
113
+ if (call_user_func($callback,$x,$param)) $res[] = $x;
114
+ }
115
+ return $res;
116
+ }
117
+
118
+ static function __cmp_filter_usa_zip($element, $needle){
119
+ $intZip = intval($needle);
120
+ return ($intZip > $element[1] && $intZip < $element[2]) || $intZip == $element[1] || $intZip == $element[2];
121
+ }
122
+
123
+ public function getUSAStateByZip($zipCode){
124
+ if (!$this->isValidUSAZip($zipCode))
125
+ throw new InvalidArgumentException("Given code $zipCode is not valid USA ZIP code");
126
+ if (strpos($zipCode,'-'))
127
+ $zipCode = substr($zipCode, 0,strpos($zipCode, '-'));
128
+ $result = $this->find_in_array($this->USA_CODES_TABLE, "RegionResolver::__cmp_filter_usa_zip", $zipCode);
129
+ if (count($result)==0) return null;
130
+ $keys = array_keys($result);
131
+ return $result[$keys[0]][self::C_USA_STATE_ABBREVIATION];
132
+ }
133
+
134
+ static function __cmp_filter_ca_postal_code($element, $needle){
135
+ return in_array($needle,$element[1]);
136
+ }
137
+
138
+ public function getCanadianProvinceInfoByPostalCode($postalCode){
139
+ if (!$this->isValidCanadianPostalCode($postalCode)){
140
+ throw new InvalidArgumentException("Given code $postalCode is not valid Canadian postal code");
141
+ }
142
+ $districtId = strtoupper(substr($postalCode,0,1));
143
+ $result = $this->find_in_array($this->CANADA_CODES_TABLE, "RegionResolver::__cmp_filter_ca_postal_code", $districtId);
144
+ if (count($result)==0) return null;
145
+ $keys = array_keys($result);
146
+ return $result[$keys[0]];
147
+ }
148
+
149
+ public function getCanadianProvinceAbbrByPostalCode($postalCode){
150
+ $provinceInfo = $this->getCanadianProvinceInfoByPostalCode($postalCode);
151
+ if ($provinceInfo == null) return null;
152
+ return $provinceInfo[self::C_CAN_REGION_ABBREVIATION];
153
+ }
154
+
155
+ public function getCanadianProvinceNameByPostalCode($postalCode){
156
+ $provinceInfo = $this->getCanadianProvinceInfoByPostalCode($postalCode);
157
+ if ($provinceInfo == null) return null;
158
+ return $provinceInfo[self::C_CAN_REGION_NAME];
159
+ }
160
+
161
+ /**
162
+ * Determines State or Province by given postal code. If cod if not valid USA or Canadian postal cod - returns null
163
+ * @param $postalCode
164
+ * @return USA State or name of the Canadian province
165
+ */
166
+ public function getStateOrProvinceByCode($postalCode){
167
+ if ($this->isValidUSAZip($postalCode)){
168
+ return $this->getUSAStateByZip($postalCode);
169
+ }else if($this->isValidCanadianPostalCode($postalCode)) {
170
+ return $this->getCanadianProvinceNameByPostalCode($postalCode);
171
+ }else{
172
+ return null;
173
+ }
174
+ }
175
+ }
lib/ExactorCommons/config.php CHANGED
@@ -6,6 +6,6 @@
6
  */
7
 
8
  ExactorLoggingFactory::getInstance()->setup('MagentoLogger', IExactorLogger::TRACE);
9
- ExactorConnectionFactory::getInstance()->setup('Magento','v20120723');
10
  ExactorProcessingServiceFactory::getInstance()->setup(new MagentoExactorCallback());
11
 
6
  */
7
 
8
  ExactorLoggingFactory::getInstance()->setup('MagentoLogger', IExactorLogger::TRACE);
9
+ ExactorConnectionFactory::getInstance()->setup('Magento','v20120920');
10
  ExactorProcessingServiceFactory::getInstance()->setup(new MagentoExactorCallback());
11
 
package.xml CHANGED
@@ -1,7 +1,7 @@
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>Mage_Exactor_Tax</name>
4
- <version>2012.07.23</version>
5
  <stability>stable</stability>
6
  <license uri="http://www.opensource.org/licenses/osl-3.0.php">OSL v3.0</license>
7
  <channel>community</channel>
@@ -10,11 +10,11 @@
10
  <description>This module was developed to enable businesses to incorporate and benefit from the Exactor sales tax compliance service within their Magento shopping cart. Exactor presents a sales tax compliance module for (i) calculating sales tax within the e-commerce shopping process; and (ii) generating and filing sales tax returns in all US jurisdictions (state and local).This plug-in was developed to enable businesses to incorporate and benefit from the Exactor sales tax compliance service within their Magento shopping cart. &#xD;
11
  Once installed, neither the merchant, nor the customer, need to perform any additional functions to obtain sales taxes. The taxes will be calculated and seamlessly displayed in the Magento shopping cart, and because the shopping cart communicates with the Exactor system, the ExactorFile module will be able to generate tax returns without the merchant having to transfer data from one system to another.&#xD;
12
  For additional information, please refer to the Exactor Magento User Guide that is attached to the plug-in, or which you can download directly from the Exactor control panel (navigate to Account Management Integration Points &amp; PlugIns).</description>
13
- <notes>Supported Magento 1.5.0.0 - 1.7.x</notes>
14
  <authors><author><name>Exactor, Inc.</name><user>exactor</user><email>support@exactor.com</email></author></authors>
15
- <date>2012-07-23</date>
16
- <time>16:15:21</time>
17
- <contents><target name="magelocal"><dir name="Exactor"><dir name="Core"><dir name="Helper"><file name="SessionCache.php" hash="4aab06767188e8ead64043654989473d"/></dir><dir name="Model"><file name="ExactorTransaction.php" hash="852aa20f6e3b7aa0001439d4bffe9724"/><file name="MerchantSettings.php" hash="b4c2acde5dfa929e89ea7ec9fe0f1b2f"/><dir name="Mysql4"><dir name="ExactorTransaction"><file name="Collection.php" hash="c7b890b4d3ab35e65a3856ae0e2fdf72"/></dir><file name="ExactorTransaction.php" hash="c91aebaae767613acf1c439a8b513c3b"/><dir name="MerchantSettings"><file name="Collection.php" hash="c1593f52e582e601409c4651c37495af"/><file name="Collection.php~" hash="d360e202eb30432ac41dc023ce13ffc0"/></dir><file name="MerchantSettings.php" hash="8f22b76bb64cdc7b1deb6f0e38889905"/></dir><dir name="Type"><file name="Onepage.php" hash="2441fb08bbeb579831cea4d3fbd31104"/></dir></dir><dir name="etc"><file name="config.xml" hash="98ecc82a5f7b74ac0bfc0f7ab512afdb"/></dir></dir><dir name="ExactorSettings"><dir name="Block"><file name="Form.php" hash="7dcfb00922cfe305d8ae08cb20ca5e87"/></dir><dir name="Helper"><file name="Data.php" hash="a23bb9c251f0dbb77c107d01dde39e86"/><file name="VersionResolver.php" hash="14dce068dfe2a7d3364c4bd29e6f8431"/></dir><dir name="controllers"><dir name="Adminhtml"><file name="FormController.php" hash="a154f13f8c4c04f838dfb79607078341"/></dir><file name="AjaxController.php" hash="c07aeeca00e1408e52945fc027569793"/><file name="IndexController.php" hash="f47cbc274dd68c57c30b60bbee69259e"/></dir><dir name="etc"><file name="config.xml" hash="6c2f28c302b359abd12565264fa23bc5"/></dir><dir name="sql"><dir name="ExactorSettings_setup"><file name="mysql4-install-14.04.2012.php" hash="f35af1e12921b57479cb4b5677937a0c"/></dir></dir></dir><dir name="Sales"><dir name="Model"><file name="Observer.php" hash="597c015390a1c0d8e327aaa17d756be3"/></dir><dir name="etc"><file name="config.xml" hash="38b42ea182d95847903f9ddfda67b282"/></dir></dir><dir name="Tax"><dir name="Helper"><file name="Calculation.php" hash="29c5252bdd48b173c90588f449114024"/><file name="Mapping.php" hash="31e761acdecabb2cbfd744c88c60fcbe"/></dir><dir name="Model"><dir name="Sales"><dir name="Total"><dir name="Quote"><dir name="Nominal"><file name="Tax.php" hash="156eff380df5b16db55449759f193490"/></dir><file name="Tax.php" hash="15fce92e8f6bc2caf0ce09df51cf81a4"/></dir></dir></dir></dir><dir name="etc"><file name="config.xml" hash="9a653c98a67e3743f45618d0890aab00"/></dir></dir></dir></target><target name="magedesign"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="layout"><file name="exactorsettings.xml" hash="3707eac08d2393c8796eebf1cf7a0bd9"/><file name="exactoronestepcheckout.xml" hash="9fdfa1db5e4e60b4eec8f348c10aab07"/></dir><dir name="template"><dir name="ExactorSettings"><file name="settingsform.phtml" hash="c77a9c5461dbb37f0a8835e55f48f1d6"/></dir></dir></dir></dir></dir><dir name="frontend"><dir name="base"><dir name="default"><dir name="layout"><file name="exactoronestepcheckout.xml" hash="9fdfa1db5e4e60b4eec8f348c10aab07"/></dir></dir></dir></dir></target><target name="mageetc"><dir name="modules"><file name="Exactor.xml" hash="e8997e8e36a265141b37790caaef39d6"/></dir></target><target name="mageweb"><dir name="js"><dir name="exactor"><file name="exactor.js" hash="5c23e40f4034e50a6e0df5b1c708b7e1"/></dir></dir></target><target name="magelib"><dir name="ExactorCommons"><file name="ExactorCommons.php" hash="1a9ffaa25007a2b6eb170905ecaed97e"/><file name="ExactorDomainObjects.php" hash="d69c2827a03213386b861076ad75e33a"/><file name="Magento.php" hash="76da7333fe0692053a47f8564c7b513a"/><file name="XmlProcessing.php" hash="383fd21839889d720e2094e83ccc7a2a"/><file name="config.php" hash="b33630997120f36b5470626f98dc372a"/><file name="config.php~" hash="f12c65295b05793efd89dd64748282d1"/></dir></target></contents>
18
  <compatible/>
19
  <dependencies><required><php><min>5.0.0</min><max>6.0.0</max></php></required></dependencies>
20
  </package>
1
  <?xml version="1.0"?>
2
  <package>
3
  <name>Mage_Exactor_Tax</name>
4
+ <version>2012.09.20</version>
5
  <stability>stable</stability>
6
  <license uri="http://www.opensource.org/licenses/osl-3.0.php">OSL v3.0</license>
7
  <channel>community</channel>
10
  <description>This module was developed to enable businesses to incorporate and benefit from the Exactor sales tax compliance service within their Magento shopping cart. Exactor presents a sales tax compliance module for (i) calculating sales tax within the e-commerce shopping process; and (ii) generating and filing sales tax returns in all US jurisdictions (state and local).This plug-in was developed to enable businesses to incorporate and benefit from the Exactor sales tax compliance service within their Magento shopping cart. &#xD;
11
  Once installed, neither the merchant, nor the customer, need to perform any additional functions to obtain sales taxes. The taxes will be calculated and seamlessly displayed in the Magento shopping cart, and because the shopping cart communicates with the Exactor system, the ExactorFile module will be able to generate tax returns without the merchant having to transfer data from one system to another.&#xD;
12
  For additional information, please refer to the Exactor Magento User Guide that is attached to the plug-in, or which you can download directly from the Exactor control panel (navigate to Account Management Integration Points &amp; PlugIns).</description>
13
+ <notes>Supported Magento 1.5.0.0 - 1.7.x, Magento Enterprise 1.8-1.12.x</notes>
14
  <authors><author><name>Exactor, Inc.</name><user>exactor</user><email>support@exactor.com</email></author></authors>
15
+ <date>2012-09-21</date>
16
+ <time>16:32:09</time>
17
+ <contents><target name="magelocal"><dir name="Exactor"><dir name="Core"><dir name="Helper"><file name="SessionCache.php" hash="72692a33574f8ba2f1858e2e93aa4edf"/></dir><dir name="Model"><file name="ExactorTransaction.php" hash="852aa20f6e3b7aa0001439d4bffe9724"/><file name="MerchantSettings.php" hash="b4c2acde5dfa929e89ea7ec9fe0f1b2f"/><dir name="Mysql4"><dir name="ExactorTransaction"><file name="Collection.php" hash="c7b890b4d3ab35e65a3856ae0e2fdf72"/></dir><file name="ExactorTransaction.php" hash="c91aebaae767613acf1c439a8b513c3b"/><dir name="MerchantSettings"><file name="Collection.php" hash="c1593f52e582e601409c4651c37495af"/><file name="Collection.php~" hash="d360e202eb30432ac41dc023ce13ffc0"/></dir><file name="MerchantSettings.php" hash="8f22b76bb64cdc7b1deb6f0e38889905"/></dir><dir name="Type"><file name="Onepage.php" hash="2441fb08bbeb579831cea4d3fbd31104"/></dir></dir><dir name="etc"><file name="config.xml" hash="98ecc82a5f7b74ac0bfc0f7ab512afdb"/></dir></dir><dir name="ExactorSettings"><dir name="Block"><file name="Form.php" hash="7dcfb00922cfe305d8ae08cb20ca5e87"/></dir><dir name="Helper"><file name="Data.php" hash="a23bb9c251f0dbb77c107d01dde39e86"/><file name="VersionResolver.php" hash="14dce068dfe2a7d3364c4bd29e6f8431"/></dir><dir name="controllers"><dir name="Adminhtml"><file name="FormController.php" hash="bebd9e556ff2266f5208c950da3ade14"/></dir><file name="AjaxController.php" hash="c07aeeca00e1408e52945fc027569793"/><file name="IndexController.php" hash="f47cbc274dd68c57c30b60bbee69259e"/></dir><dir name="etc"><file name="config.xml" hash="6c2f28c302b359abd12565264fa23bc5"/></dir><dir name="sql"><dir name="ExactorSettings_setup"><file name="mysql4-install-14.04.2012.php" hash="f35af1e12921b57479cb4b5677937a0c"/></dir></dir></dir><dir name="Sales"><dir name="Model"><file name="Observer.php" hash="3bf71b114fd042f9da4cafd0e2f60712"/></dir><dir name="etc"><file name="config.xml" hash="621a49d9f295a607fadb1cd92a96cbe0"/></dir></dir><dir name="Tax"><dir name="Helper"><file name="Calculation.php" hash="29c5252bdd48b173c90588f449114024"/><file name="Mapping.php" hash="6246788c22bfbf4f26a38940fa392f92"/></dir><dir name="Model"><dir name="Sales"><dir name="Total"><dir name="Quote"><dir name="Nominal"><file name="Tax.php" hash="156eff380df5b16db55449759f193490"/></dir><file name="Tax.php" hash="2177508f9375ba7c250ae165a798de72"/></dir></dir></dir></dir><dir name="etc"><file name="config.xml" hash="d4bc1466549b6a860cdc6793103ebfb5"/></dir></dir></dir></target><target name="magedesign"><dir name="adminhtml"><dir name="default"><dir name="default"><dir name="layout"><file name="exactorsettings.xml" hash="3707eac08d2393c8796eebf1cf7a0bd9"/><file name="exactoronestepcheckout.xml" hash="9fdfa1db5e4e60b4eec8f348c10aab07"/></dir><dir name="template"><dir name="ExactorSettings"><file name="settingsform.phtml" hash="40d421317cee3a5a8df1dde2b223f7d9"/></dir></dir></dir></dir></dir><dir name="frontend"><dir name="base"><dir name="default"><dir name="layout"><file name="exactoronestepcheckout.xml" hash="9fdfa1db5e4e60b4eec8f348c10aab07"/></dir></dir></dir></dir></target><target name="mageetc"><dir name="modules"><file name="Exactor.xml" hash="e8997e8e36a265141b37790caaef39d6"/></dir></target><target name="mageweb"><dir name="js"><dir name="exactor"><file name="exactor.js" hash="5c23e40f4034e50a6e0df5b1c708b7e1"/></dir></dir></target><target name="magelib"><dir name="ExactorCommons"><file name="ExactorCommons.php" hash="8c1fb7bc67a16e47d99c957c5d19b4be"/><file name="ExactorDomainObjects.php" hash="28be782abfd7cf4bd4d23228ae97acc8"/><file name="Magento.php" hash="76da7333fe0692053a47f8564c7b513a"/><file name="RegionResolver.php" hash="2537638a7895a169cee4b1df786d22fb"/><file name="XmlProcessing.php" hash="383fd21839889d720e2094e83ccc7a2a"/><file name="config.php" hash="2d433e23d3b6588cc71806e5a2c1b6c7"/><file name="config.php~" hash="f12c65295b05793efd89dd64748282d1"/></dir></target></contents>
18
  <compatible/>
19
  <dependencies><required><php><min>5.0.0</min><max>6.0.0</max></php></required></dependencies>
20
  </package>