├── modman ├── aoe-logo.png ├── app ├── code │ └── community │ │ └── Aoe │ │ └── CartApi │ │ ├── Model │ │ ├── Cart │ │ │ └── Rest │ │ │ │ ├── Guest │ │ │ │ └── V1.php │ │ │ │ └── Customer │ │ │ │ └── V1.php │ │ ├── Item │ │ │ └── Rest │ │ │ │ ├── Guest │ │ │ │ └── V1.php │ │ │ │ └── Customer │ │ │ │ └── V1.php │ │ ├── Place │ │ │ └── Rest │ │ │ │ ├── Guest │ │ │ │ └── V1.php │ │ │ │ └── Customer │ │ │ │ └── V1.php │ │ ├── Payment │ │ │ └── Rest │ │ │ │ ├── Guest │ │ │ │ └── V1.php │ │ │ │ └── Customer │ │ │ │ └── V1.php │ │ ├── Validate │ │ │ └── Rest │ │ │ │ ├── Guest │ │ │ │ └── V1.php │ │ │ │ └── Customer │ │ │ │ └── V1.php │ │ ├── Crosssell │ │ │ └── Rest │ │ │ │ ├── Guest │ │ │ │ └── V1.php │ │ │ │ └── Customer │ │ │ │ └── V1.php │ │ ├── BillingAddress │ │ │ └── Rest │ │ │ │ ├── Guest │ │ │ │ └── V1.php │ │ │ │ └── Customer │ │ │ │ └── V1.php │ │ ├── PaymentMethods │ │ │ └── Rest │ │ │ │ ├── Guest │ │ │ │ └── V1.php │ │ │ │ └── Customer │ │ │ │ └── V1.php │ │ ├── ShippingAddress │ │ │ └── Rest │ │ │ │ ├── Guest │ │ │ │ └── V1.php │ │ │ │ └── Customer │ │ │ │ └── V1.php │ │ ├── ShippingMethod │ │ │ └── Rest │ │ │ │ ├── Guest │ │ │ │ └── V1.php │ │ │ │ └── Customer │ │ │ │ └── V1.php │ │ ├── Validate.php │ │ ├── PaymentMethods.php │ │ ├── ShippingMethod.php │ │ ├── Payment.php │ │ ├── Place.php │ │ ├── Resource.php │ │ ├── BillingAddress.php │ │ ├── ShippingAddress.php │ │ ├── Crosssell.php │ │ ├── Cart.php │ │ └── Item.php │ │ ├── etc │ │ ├── config.xml │ │ ├── system.xml │ │ └── api2.xml │ │ └── Helper │ │ └── Data.php └── etc │ └── modules │ └── Aoe_CartApi.xml ├── scripts ├── get.sh ├── delete.sh ├── post_raw.sh ├── post_kv.sh └── login.sh ├── .travis.yml ├── composer.json ├── phpcs.xml ├── LICENSE.txt └── README.md /modman: -------------------------------------------------------------------------------- 1 | app/code/community/Aoe/CartApi 2 | app/etc/modules/Aoe_CartApi.xml 3 | -------------------------------------------------------------------------------- /aoe-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AOEpeople/Aoe_CartApi/HEAD/aoe-logo.png -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/Cart/Rest/Guest/V1.php: -------------------------------------------------------------------------------- 1 | " 8 | exit 1 9 | fi 10 | 11 | echo "GET ${URL}" 12 | curl -v -s -H "Content-Type: application/json" -H "Cookie: frontend=${SESSION}; ${COOKIE}" -X GET "${URL}" | jq '.' 13 | -------------------------------------------------------------------------------- /scripts/delete.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | URL=$1 4 | SESSION=$2 5 | COOKIE=$3 6 | if [ "${URL}" == "" -o "${SESSION}" == "" ]; then 7 | echo "Usage: $0 " 8 | exit 1 9 | fi 10 | 11 | echo "DELETE ${URL}" 12 | curl -v -s -H "Content-Type: application/json" -H "Cookie: frontend=${SESSION}; ${COOKIE}" -X DELETE "${URL}" | jq '.' 13 | -------------------------------------------------------------------------------- /scripts/post_raw.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | URL=$1 4 | SESSION=$2 5 | DATA=$3 6 | COOKIE=$4 7 | 8 | if [ "${URL}" == "" -o "${SESSION}" == "" -o "${DATA}" == "" ]; then 9 | echo "Usage: $0 " 10 | exit 1 11 | fi 12 | 13 | echo "POST ${URL}" 14 | curl -v -H "Content-Type: application/json" -H "Cookie: frontend=${SESSION}; ${COOKIE}" -X POST -d "${DATA}" "${URL}" | jq '.' 15 | -------------------------------------------------------------------------------- /scripts/post_kv.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | URL=$1 4 | SESSION=$2 5 | KEY=$3 6 | VALUE=$4 7 | COOKIE=$5 8 | 9 | if [ "${URL}" == "" -o "${SESSION}" == "" -o "${KEY}" == "" ]; then 10 | echo "Usage: $0 []" 11 | exit 1 12 | fi 13 | 14 | echo "POST ${URL}" 15 | DATA="{\"${KEY}\":\"${VALUE}\"}" 16 | curl -v -H "Content-Type: application/json" -H "Cookie: frontend=${SESSION}; ${COOKIE}" -X POST -d "${DATA}" "${URL}" | jq '.' 17 | -------------------------------------------------------------------------------- /scripts/login.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | URL=$1 4 | USERNAME=$2 5 | PASSWORD=$3 6 | COOKIE=$4 7 | 8 | if [ "${URL}" == "" -o "${USERNAME}" == "" -o "${PASSWORD}" == "" ]; then 9 | echo "Usage: $0 " 10 | exit 1 11 | fi 12 | 13 | DATA="{\"login\":\"${USERNAME}\", \"password\":\"${PASSWORD}\"}" 14 | SESSION=`curl -s -H "Cookie: ${COOKIE}" -H "Content-Type: application/json" -X POST -d "${DATA}" -D - "${URL}/session/customer" | grep 'Set-Cookie: frontend=' | tail -n1 | sed -En 's/^Set-Cookie: frontend=([^;]+).*/\1/p'` 15 | 16 | echo $SESSION 17 | -------------------------------------------------------------------------------- /app/etc/modules/Aoe_CartApi.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | community 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | env: 6 | matrix: 7 | - MAGENTO_VERSION=magento-ce-1.9.1.0 8 | - MAGENTO_VERSION=magento-ce-1.9.0.1 9 | - MAGENTO_VERSION=magento-ce-1.8.1.0 10 | - MAGENTO_VERSION=magento-ce-1.8.0.0 11 | branches: 12 | except: 13 | - /^(\d+\.)+\d+$/ 14 | before_script: 15 | - curl -OL https://squizlabs.github.io/PHP_CodeSniffer/phpcs.phar 16 | script: 17 | # Code Style 18 | - php phpcs.phar --standard=./phpcs.xml --encoding=utf-8 --report-width=180 ./app 19 | # Unit Tests 20 | #- curl -sSL https://raw.githubusercontent.com/AOEpeople/MageTestStand/master/setup.sh | bash 21 | notifications: 22 | email: 23 | recipients: [ lee.saferite@aoe.com ] 24 | on_success: always 25 | on_failure: always 26 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 0.0.1 6 | 7 | 8 | 9 | 10 | 11 | Aoe_CartApi_Helper 12 | 13 | 14 | 15 | 16 | Aoe_CartApi_Model 17 | 18 | 19 | 20 | 21 | 22 | 23 | 100 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aoepeople/aoe_cartapi", 3 | "description": "Magento REST(ish) Cart API based on Mage_Api2", 4 | "authors": [ 5 | { 6 | "name": "Fabrizio Branca", 7 | "email": "{firstname}.{lastname}@aoe.com" 8 | }, 9 | { 10 | "name": "Lee Saferite", 11 | "email": "lee.saferite@aoe.com" 12 | } 13 | ], 14 | "license": "OSL-3.0", 15 | "type": "magento-module", 16 | "require": { 17 | "php": ">= 5.4", 18 | "magento-hackathon/magento-composer-installer": "*", 19 | "aoepeople/aoe_api2": "*" 20 | }, 21 | "require-dev": { 22 | "ecomdev/ecomdev_phpunit": "*", 23 | "squizlabs/php_codesniffer": "*", 24 | "phpmd/phpmd": "*" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/etc/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Maximum width thumbnail product image will be scaled down to in pixels 11 | text 12 | 30 13 | 1 14 | 1 15 | 1 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Magento extension ruleset based on PSR-2 but modified for Magento 1 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/Validate.php: -------------------------------------------------------------------------------- 1 | loadQuote(); 25 | 26 | switch ($this->getActionType() . $this->getOperation()) { 27 | case self::ACTION_TYPE_ENTITY . self::OPERATION_CREATE: 28 | $data = $this->validateQuote($quote); 29 | $this->saveQuote(); 30 | if ($data['status'] === 'success') { 31 | $this->getResponse()->setHttpResponseCode(Mage_Api2_Model_Server::HTTP_OK); 32 | } else { 33 | $this->getResponse()->setHttpResponseCode(422); 34 | } 35 | $this->_render($data); 36 | break; 37 | default: 38 | $this->_critical(self::RESOURCE_METHOD_NOT_ALLOWED); 39 | } 40 | } 41 | 42 | protected function validateQuote(Mage_Sales_Model_Quote $quote) 43 | { 44 | // Get a filter instance 45 | $filter = $this->getFilter(); 46 | 47 | // Fire event - before place 48 | Mage::dispatchEvent('aoe_cartapi_cart_validate_before', ['filter' => $filter, 'quote' => $quote]); 49 | 50 | // Run the validation code 51 | $errors = $this->getHelper()->validateQuote($quote); 52 | 53 | // Generate response 54 | $data = new Varien_Object(['status' => (empty($errors) ? 'success' : 'error'), 'errors' => $errors]); 55 | 56 | // Fire event - after place 57 | Mage::dispatchEvent('aoe_cartapi_cart_validate_after', ['filter' => $filter, 'quote' => $quote, 'data' => $data]); 58 | 59 | // Get response data 60 | $data = $data->getData(); 61 | 62 | // Filter outbound data 63 | $data = $filter->out($data); 64 | 65 | // Fix data types 66 | $data = $this->fixTypes($data); 67 | 68 | // Add null values for missing data 69 | foreach ($filter->getAttributesToInclude() as $code) { 70 | if (!array_key_exists($code, $data)) { 71 | $data[$code] = null; 72 | } 73 | } 74 | 75 | // Sort the result by key 76 | ksort($data); 77 | 78 | return new ArrayObject($data); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/PaymentMethods.php: -------------------------------------------------------------------------------- 1 | loadQuote(); 26 | 27 | switch ($this->getActionType() . $this->getOperation()) { 28 | case self::ACTION_TYPE_COLLECTION . self::OPERATION_RETRIEVE: 29 | $this->_render($this->prepareCollection($quote)); 30 | break; 31 | default: 32 | $this->_critical(self::RESOURCE_METHOD_NOT_ALLOWED); 33 | } 34 | } 35 | 36 | /** 37 | * Convert the resource model collection to an array 38 | * 39 | * @param Mage_Sales_Model_Quote $quote 40 | * 41 | * @return array 42 | */ 43 | public function prepareCollection(Mage_Sales_Model_Quote $quote) 44 | { 45 | // Store current state 46 | $actionType = $this->getActionType(); 47 | $operation = $this->getOperation(); 48 | 49 | // Change state 50 | $this->setActionType(self::ACTION_TYPE_COLLECTION); 51 | $this->setOperation(self::OPERATION_RETRIEVE); 52 | 53 | $data = []; 54 | 55 | // Get store 56 | $store = $quote->getStoreId(); 57 | 58 | // Get filter 59 | $filter = $this->getFilter(); 60 | 61 | // Prepare methods 62 | foreach (Mage::helper('payment')->getStoreMethods($store, $quote) as $method) { 63 | /** @var $method Mage_Payment_Model_Method_Abstract */ 64 | if ($this->_canUseMethod($method, $quote) && $method->isApplicableToQuote( 65 | $quote, 66 | Mage_Payment_Model_Method_Abstract::CHECK_ZERO_TOTAL 67 | )) { 68 | $method->setInfoInstance($quote->getPayment()); 69 | $data[] = $this->prepareMethod($method, $filter); 70 | } 71 | } 72 | 73 | // Restore old state 74 | $this->setActionType($actionType); 75 | $this->setOperation($operation); 76 | 77 | // Return prepared outbound data 78 | return $data; 79 | } 80 | 81 | /** 82 | * Prepare resource and return results 83 | * 84 | * @param Mage_Payment_Model_Method_Abstract $method 85 | * @param Mage_Api2_Model_Acl_Filter $filter 86 | * 87 | * @return array 88 | */ 89 | public function prepareMethod(Mage_Payment_Model_Method_Abstract $method, Mage_Api2_Model_Acl_Filter $filter) 90 | { 91 | // Get raw outbound data 92 | $data = []; 93 | $attributes = $filter->getAttributesToInclude(); 94 | $attributes = array_combine($attributes, $attributes); 95 | $attributes = array_merge($attributes, array_intersect_key($this->attributeMap, $attributes)); 96 | foreach ($attributes as $externalKey => $internalKey) { 97 | if ($externalKey === 'cc_types') { 98 | $data[$externalKey] = $this->_getPaymentMethodAvailableCcTypes($method); 99 | } else { 100 | $data[$externalKey] = $method->getDataUsingMethod($internalKey); 101 | } 102 | } 103 | 104 | // Fire event 105 | $data = new Varien_Object($data); 106 | Mage::dispatchEvent('aoe_cartapi_payment_method_prepare', ['data' => $data, 'filter' => $filter, 'resource' => $method]); 107 | $data = $data->getData(); 108 | 109 | // Filter outbound data 110 | $data = $filter->out($data); 111 | 112 | // Fix data types 113 | $data = $this->fixTypes($data); 114 | 115 | // Add null values for missing data 116 | foreach ($filter->getAttributesToInclude() as $code) { 117 | if (!array_key_exists($code, $data)) { 118 | $data[$code] = null; 119 | } 120 | } 121 | 122 | // Sort the result by key 123 | ksort($data); 124 | 125 | return $data; 126 | } 127 | 128 | /** 129 | * Check payment method model 130 | * 131 | * @param Mage_Payment_Model_Method_Abstract $method 132 | * @param Mage_Sales_Model_Quote $quote 133 | * @return bool 134 | */ 135 | protected function _canUseMethod($method, $quote) 136 | { 137 | return $method->isApplicableToQuote($quote, Mage_Payment_Model_Method_Abstract::CHECK_USE_FOR_COUNTRY 138 | | Mage_Payment_Model_Method_Abstract::CHECK_USE_FOR_CURRENCY 139 | | Mage_Payment_Model_Method_Abstract::CHECK_ORDER_TOTAL_MIN_MAX); 140 | } 141 | 142 | /** 143 | * Get available CC types for payment method 144 | * 145 | * @param $method 146 | * @return null 147 | */ 148 | protected function _getPaymentMethodAvailableCcTypes($method) 149 | { 150 | $ccTypes = Mage::getSingleton('payment/config')->getCcTypes(); 151 | $methodCcTypes = explode(',', $method->getConfigData('cctypes')); 152 | foreach ($ccTypes as $code => $title) { 153 | if (!in_array($code, $methodCcTypes)) { 154 | unset($ccTypes[$code]); 155 | } 156 | } 157 | if (empty($ccTypes)) { 158 | return null; 159 | } 160 | 161 | return $ccTypes; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/ShippingMethod.php: -------------------------------------------------------------------------------- 1 | 'method_description', 12 | ]; 13 | 14 | /** 15 | * Hash of external attribute codes and their data type 16 | * 17 | * @var string[] 18 | */ 19 | protected $attributeTypeMap = [ 20 | 'carrier' => 'string', 21 | 'carrier_title' => 'string', 22 | 'method' => 'string', 23 | 'method_title' => 'string', 24 | 'description' => 'string', 25 | 'price' => 'currency', 26 | ]; 27 | 28 | /** 29 | * Dispatch API call 30 | */ 31 | public function dispatch() 32 | { 33 | $quote = $this->loadQuote(); 34 | 35 | switch ($this->getActionType() . $this->getOperation()) { 36 | case self::ACTION_TYPE_COLLECTION . self::OPERATION_RETRIEVE: 37 | $this->_render($this->prepareCollection($quote)); 38 | break; 39 | default: 40 | $this->_critical(self::RESOURCE_METHOD_NOT_ALLOWED); 41 | } 42 | } 43 | 44 | /** 45 | * Convert the resource model collection to an array 46 | * 47 | * @param Mage_Sales_Model_Quote $quote 48 | * 49 | * @return array 50 | */ 51 | public function prepareCollection(Mage_Sales_Model_Quote $quote) 52 | { 53 | if ($quote->isVirtual()) { 54 | return []; 55 | } 56 | 57 | // Store current state 58 | $actionType = $this->getActionType(); 59 | $operation = $this->getOperation(); 60 | 61 | // Change state 62 | $this->setActionType(self::ACTION_TYPE_COLLECTION); 63 | $this->setOperation(self::OPERATION_RETRIEVE); 64 | 65 | $data = []; 66 | // Load and prep shipping address 67 | $address = $quote->getShippingAddress(); 68 | $address->setCollectShippingRates(true); 69 | $address->collectShippingRates(); 70 | $address->save(); 71 | 72 | // Load rates 73 | /** @var Mage_Sales_Model_Resource_Quote_Address_Rate_Collection $rateCollection */ 74 | $rateCollection = $address->getShippingRatesCollection(); 75 | $rates = []; 76 | foreach ($rateCollection as $rate) { 77 | /** @var Mage_Sales_Model_Quote_Address_Rate $rate */ 78 | if (!$rate->isDeleted() && $rate->getCarrierInstance()) { 79 | $rates[] = $rate; 80 | } 81 | } 82 | uasort($rates, [$this, 'sortRates']); 83 | 84 | // Get filter 85 | $filter = $this->getFilter(); 86 | 87 | // Prepare rates 88 | foreach ($rates as $rate) { 89 | /** @var Mage_Sales_Model_Quote_Address_Rate $rate */ 90 | $data[] = $this->prepareRate($rate, $filter); 91 | } 92 | 93 | // Restore old state 94 | $this->setActionType($actionType); 95 | $this->setOperation($operation); 96 | 97 | // Return prepared outbound data 98 | return $data; 99 | } 100 | 101 | protected function prepareRate(Mage_Sales_Model_Quote_Address_Rate $rate, Mage_Api2_Model_Acl_Filter $filter) 102 | { 103 | // Get raw outbound data 104 | $data = []; 105 | $attributes = $filter->getAttributesToInclude(); 106 | $attributes = array_combine($attributes, $attributes); 107 | $attributes = array_merge($attributes, array_intersect_key($this->attributeMap, $attributes)); 108 | foreach ($attributes as $externalKey => $internalKey) { 109 | $data[$externalKey] = $rate->getDataUsingMethod($internalKey); 110 | } 111 | 112 | // Fire event 113 | $data = new Varien_Object($data); 114 | Mage::dispatchEvent('aoe_cartapi_shipping_method_prepare', ['data' => $data, 'filter' => $filter, 'resource' => $rate]); 115 | $data = $data->getData(); 116 | 117 | // Filter outbound data 118 | $data = $filter->out($data); 119 | 120 | // Fix data types 121 | $data = $this->fixTypes($data); 122 | 123 | // Add null values for missing data 124 | foreach ($filter->getAttributesToInclude() as $code) { 125 | if (!array_key_exists($code, $data)) { 126 | $data[$code] = null; 127 | } 128 | } 129 | 130 | // Sort the result by key 131 | ksort($data); 132 | 133 | return $data; 134 | } 135 | 136 | protected function sortRates(Mage_Sales_Model_Quote_Address_Rate $a, Mage_Sales_Model_Quote_Address_Rate $b) 137 | { 138 | // Sort by price (lowest first) 139 | // This is a crappy solution and should be rewritten 140 | $aSort = intval(round(floatval($a->getPrice()) * 10000)); 141 | $bSort = intval(round(floatval($b->getPrice()) * 10000)); 142 | if ($aSort < $bSort) { 143 | return -1; 144 | } elseif ($aSort > $bSort) { 145 | return 1; 146 | } 147 | 148 | // Sory by carrier order (lowest first) 149 | $aSort = $a->getCarrierInstance()->getSortOrder(); 150 | $bSort = $b->getCarrierInstance()->getSortOrder(); 151 | if ($aSort < $bSort) { 152 | return -1; 153 | } elseif ($aSort > $bSort) { 154 | return 1; 155 | } 156 | 157 | // Sort my method order (lowest first) 158 | $aSort = intval($a->getSortOrder()); 159 | $bSort = intval($b->getSortOrder()); 160 | if ($aSort < $bSort) { 161 | return -1; 162 | } elseif ($aSort > $bSort) { 163 | return 1; 164 | } 165 | 166 | return 0; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/Payment.php: -------------------------------------------------------------------------------- 1 | loadQuote()->getPayment(); 25 | 26 | switch ($this->getActionType() . $this->getOperation()) { 27 | case self::ACTION_TYPE_ENTITY . self::OPERATION_RETRIEVE: 28 | $this->_render($this->prepareResource($resource)); 29 | break; 30 | case self::ACTION_TYPE_ENTITY . self::OPERATION_CREATE: 31 | case self::ACTION_TYPE_ENTITY . self::OPERATION_UPDATE: 32 | $this->updateResource($resource, $this->getRequest()->getBodyParams()); 33 | $this->saveQuote(); 34 | $this->_render($this->prepareResource($resource)); 35 | break; 36 | case self::ACTION_TYPE_ENTITY . self::OPERATION_DELETE: 37 | $resource->delete(); 38 | $this->getResponse()->setMimeType($this->getRenderer()->getMimeType()); 39 | $this->getResponse()->setHttpResponseCode(204); 40 | break; 41 | default: 42 | $this->_critical(self::RESOURCE_METHOD_NOT_ALLOWED); 43 | } 44 | } 45 | 46 | /** 47 | * @param Mage_Sales_Model_Quote_Payment $resource 48 | * 49 | * @return array 50 | */ 51 | public function prepareResource(Mage_Sales_Model_Quote_Payment $resource) 52 | { 53 | // Store current state 54 | $actionType = $this->getActionType(); 55 | $operation = $this->getOperation(); 56 | 57 | // Change state 58 | $this->setActionType(self::ACTION_TYPE_ENTITY); 59 | $this->setOperation(self::OPERATION_RETRIEVE); 60 | 61 | // Get a filter instance 62 | $filter = $this->getFilter(); 63 | 64 | // Get raw outbound data 65 | $data = $this->loadResourceAttributes($resource, $filter->getAttributesToInclude()); 66 | 67 | // Fire event 68 | $data = new Varien_Object($data); 69 | Mage::dispatchEvent('aoe_cartapi_payment_prepare', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 70 | $data = $data->getData(); 71 | 72 | // Filter outbound data 73 | $data = $filter->out($data); 74 | 75 | // Fix data types 76 | $data = $this->fixTypes($data); 77 | 78 | // Add null values for missing data 79 | foreach ($this->getFilter()->getAttributesToInclude() as $code) { 80 | if (!array_key_exists($code, $data)) { 81 | $data[$code] = null; 82 | } 83 | } 84 | 85 | // Sort the result by key 86 | ksort($data); 87 | 88 | // Restore old state 89 | $this->setActionType($actionType); 90 | $this->setOperation($operation); 91 | 92 | // Return prepared outbound data 93 | return $data; 94 | } 95 | 96 | /** 97 | * Update the resource model 98 | * 99 | * @param Mage_Sales_Model_Quote_Payment $resource 100 | * @param array $data 101 | * 102 | * @return Mage_Sales_Model_Quote_Item 103 | */ 104 | public function updateResource(Mage_Sales_Model_Quote_Payment $resource, array $data) 105 | { 106 | // Store current state 107 | $actionType = $this->getActionType(); 108 | $operation = $this->getOperation(); 109 | 110 | // Change state 111 | $this->setActionType(self::ACTION_TYPE_ENTITY); 112 | $this->setOperation(self::OPERATION_UPDATE); 113 | 114 | // Get a filter instance 115 | $filter = $this->getFilter(); 116 | 117 | // Fire event - before filter 118 | $data = new Varien_Object($data); 119 | Mage::dispatchEvent('aoe_cartapi_payment_update_before', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 120 | $data = $data->getData(); 121 | 122 | // Filter raw incoming data 123 | $data = $filter->in($data); 124 | 125 | // Clean up input format to what Magento expects 126 | if (isset($data['data']) && is_array($data['data'])) { 127 | $base = $data; 128 | unset($base['data']); 129 | $data = array_merge($data['data'], $base); 130 | } else { 131 | unset($data['data']); 132 | } 133 | 134 | // Map data keys 135 | $data = $this->mapAttributes($data); 136 | 137 | // Manual data setting 138 | $quote = $resource->getQuote(); 139 | if ($quote->isVirtual()) { 140 | $quote->getBillingAddress()->setPaymentMethod(isset($data['method']) ? $data['method'] : null); 141 | } else { 142 | $quote->getShippingAddress()->setPaymentMethod(isset($data['method']) ? $data['method'] : null); 143 | 144 | // Shipping totals may be affected by payment method 145 | $quote->getShippingAddress()->setCollectShippingRates(true); 146 | } 147 | 148 | // Define validation checks 149 | $data['checks'] = Mage_Payment_Model_Method_Abstract::CHECK_USE_CHECKOUT 150 | | Mage_Payment_Model_Method_Abstract::CHECK_USE_FOR_COUNTRY 151 | | Mage_Payment_Model_Method_Abstract::CHECK_USE_FOR_CURRENCY 152 | | Mage_Payment_Model_Method_Abstract::CHECK_ORDER_TOTAL_MIN_MAX; 153 | 154 | // Update model 155 | $resource->importData($data); 156 | 157 | // Fire event - after 158 | $data = new Varien_Object($data); 159 | Mage::dispatchEvent('aoe_cartapi_payment_update_after', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 160 | 161 | // Restore old state 162 | $this->setActionType($actionType); 163 | $this->setOperation($operation); 164 | 165 | // Return updated resource 166 | return $resource; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/Place.php: -------------------------------------------------------------------------------- 1 | loadQuote(); 25 | 26 | switch ($this->getActionType() . $this->getOperation()) { 27 | case self::ACTION_TYPE_ENTITY . self::OPERATION_CREATE: 28 | $data = $this->placeOrder($quote); 29 | if ($data['status'] === 'success') { 30 | $this->getResponse()->setHttpResponseCode(Mage_Api2_Model_Server::HTTP_CREATED); 31 | $this->getResponse()->setHeader('Location', (isset($data['url']) ? $data['url'] : '')); 32 | } else { 33 | $this->getResponse()->setHttpResponseCode(422); 34 | } 35 | $this->_render($data); 36 | break; 37 | default: 38 | $this->_critical(self::RESOURCE_METHOD_NOT_ALLOWED); 39 | } 40 | } 41 | 42 | protected function placeOrder(Mage_Sales_Model_Quote $quote) 43 | { 44 | // Get a filter instance 45 | $filter = $this->getFilter(); 46 | 47 | // Fire event - before place 48 | Mage::dispatchEvent('aoe_cartapi_cart_place_before', ['filter' => $filter, 'quote' => $quote]); 49 | 50 | // Run the validation code 51 | $errors = $this->getHelper()->validateQuote($quote); 52 | $this->saveQuote(); 53 | 54 | if (count($errors)) { 55 | // Generate response 56 | $data = new Varien_Object(['status' => 'error', 'errors' => $errors]); 57 | 58 | // Fire event - error 59 | Mage::dispatchEvent('aoe_cartapi_cart_place_error', ['filter' => $filter, 'quote' => $quote, 'data' => $data]); 60 | 61 | // Get response data 62 | $data = $data->getData(); 63 | } else { 64 | if ($quote->getCustomerId()) { 65 | $customer = $quote->getCustomer(); 66 | $billing = $quote->getBillingAddress(); 67 | 68 | if (!$billing->getCustomerId() || $billing->getSaveInAddressBook()) { 69 | $customerBilling = $billing->exportCustomerAddress(); 70 | $customer->addAddress($customerBilling); 71 | $billing->setCustomerAddress($customerBilling); 72 | } 73 | 74 | if (!$quote->isVirtual()) { 75 | $shipping = $quote->getShippingAddress(); 76 | if ($shipping->getSameAsBilling()) { 77 | // Copy data from billing address 78 | $shipping->importCustomerAddress($quote->getBillingAddress()->exportCustomerAddress()); 79 | $shipping->setSameAsBilling(1); 80 | } elseif (!$shipping->getCustomerId() || $shipping->getSaveInAddressBook()) { 81 | $customerShipping = $shipping->exportCustomerAddress(); 82 | $customer->addAddress($customerShipping); 83 | $shipping->setCustomerAddress($customerShipping); 84 | } 85 | } 86 | } else { 87 | $quote->setCustomerId(null); 88 | $quote->setCustomerEmail($quote->getBillingAddress()->getEmail()); 89 | $quote->setCustomerIsGuest(true); 90 | $quote->setCustomerGroupId(Mage_Customer_Model_Group::NOT_LOGGED_IN_ID); 91 | 92 | if (!$quote->isVirtual() && $quote->getShippingAddress()->getSameAsBilling()) { 93 | // Copy data from billing address 94 | $quote->getShippingAddress()->importCustomerAddress($quote->getBillingAddress()->exportCustomerAddress()); 95 | $quote->getShippingAddress()->setSameAsBilling(1); 96 | } 97 | } 98 | 99 | try { 100 | // Convert a quote into an order 101 | /** @var Mage_Sales_Model_Service_Quote $service */ 102 | $service = Mage::getModel('sales/service_quote', $quote); 103 | $service->submitOrder(); 104 | 105 | // Save the quote again to capture the is_active change 106 | $quote->save(); 107 | 108 | // Get the new order 109 | $order = $service->getOrder(); 110 | 111 | /** 112 | * a flag to set that there will be redirect to third party after confirmation 113 | * eg: paypal standard ipn 114 | */ 115 | $redirectUrl = $quote->getPayment()->getOrderPlaceRedirectUrl(); 116 | /** 117 | * we only want to send to customer about new order when there is no redirect to third party 118 | */ 119 | if (!$redirectUrl && $order->getCanSendNewEmailFlag()) { 120 | // Send new order email - failure is just logged 121 | try { 122 | if (method_exists($order, 'queueNewOrderEmail')) { 123 | $order->queueNewOrderEmail(); 124 | } else { 125 | $order->sendNewOrderEmail(); 126 | } 127 | } catch (Exception $e) { 128 | Mage::logException($e); 129 | } 130 | } 131 | 132 | // Generate response 133 | $data = new Varien_Object(['status' => 'success', 'order' => $order->getIncrementId(), 'url' => $redirectUrl]); 134 | 135 | // Fire event - success 136 | Mage::dispatchEvent('aoe_cartapi_cart_place_success', ['filter' => $filter, 'quote' => $quote, 'order' => $order, 'data' => $data]); 137 | 138 | // Get response data 139 | $data = $data->getData(); 140 | } catch (Mage_Payment_Model_Info_Exception $e) { 141 | // Generate response 142 | $data = new Varien_Object(['status' => 'error', 'errors' => ['payment' => [$e->getMessage()]]]); 143 | 144 | // Fire event - error 145 | Mage::dispatchEvent('aoe_cartapi_cart_place_error', ['filter' => $filter, 'quote' => $quote, 'data' => $data]); 146 | 147 | // Get response data 148 | $data = $data->getData(); 149 | } 150 | } 151 | 152 | // Fire event - after place 153 | Mage::dispatchEvent('aoe_cartapi_cart_place_after', ['filter' => $filter, 'quote' => $quote, 'data' => $data]); 154 | 155 | // Filter outbound data 156 | $data = $filter->out($data); 157 | 158 | // Fix data types 159 | $data = $this->fixTypes($data); 160 | 161 | // Add null values for missing data 162 | foreach ($filter->getAttributesToInclude() as $code) { 163 | if (!array_key_exists($code, $data)) { 164 | $data[$code] = null; 165 | } 166 | } 167 | 168 | // Sort the result by key 169 | ksort($data); 170 | 171 | return new ArrayObject($data); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/Resource.php: -------------------------------------------------------------------------------- 1 | _critical(self::RESOURCE_METHOD_NOT_ALLOWED); 36 | } 37 | 38 | /** 39 | * @inheritdoc 40 | */ 41 | public function getFilter() 42 | { 43 | $this->_filter = null; 44 | 45 | return parent::getFilter(); 46 | } 47 | 48 | /** 49 | * @return Mage_Sales_Model_Quote 50 | */ 51 | protected function loadQuote() 52 | { 53 | return $this->getHelper()->loadQuote(); 54 | } 55 | 56 | /** 57 | * @return Mage_Sales_Model_Quote 58 | */ 59 | protected function saveQuote() 60 | { 61 | return $this->getHelper()->saveQuote(); 62 | } 63 | 64 | /** 65 | * @return Aoe_CartApi_Helper_Data 66 | */ 67 | protected function getHelper() 68 | { 69 | return Mage::helper('Aoe_CartApi'); 70 | } 71 | 72 | /** 73 | * Read in the attributes from a resource 74 | * 75 | * @param Varien_Object $resource 76 | * @param string[] $attributeCodes 77 | * @param mixed[] $data 78 | * 79 | * @return mixed[] 80 | */ 81 | protected function loadResourceAttributes(Varien_Object $resource, array $attributeCodes, array $data = []) 82 | { 83 | $attributeCodes = array_diff($attributeCodes, $this->manualAttributes); 84 | $attributeCodes = array_combine($attributeCodes, $attributeCodes); 85 | $attributeCodes = array_merge($attributeCodes, array_intersect_key($this->attributeMap, $attributeCodes)); 86 | foreach ($attributeCodes as $externalKey => $internalKey) { 87 | $data[$externalKey] = $resource->getDataUsingMethod($internalKey); 88 | } 89 | 90 | return $data; 91 | } 92 | 93 | /** 94 | * Update a resource 95 | * 96 | * @param Varien_Object $resource 97 | * @param string[] $attributeCodes 98 | * @param mixed[] $data 99 | * 100 | * @return $this 101 | */ 102 | protected function saveResourceAttributes(Varien_Object $resource, array $attributeCodes, array $data) 103 | { 104 | $attributeCodes = array_diff($attributeCodes, $this->manualAttributes); 105 | $attributeCodes = array_combine($attributeCodes, $attributeCodes); 106 | $attributeCodes = array_merge($attributeCodes, array_intersect_key($this->attributeMap, $attributeCodes)); 107 | foreach (array_intersect_key($data, $attributeCodes) as $externalKey => $value) { 108 | $resource->setDataUsingMethod($attributeCodes[$externalKey], $value); 109 | } 110 | 111 | return $this; 112 | } 113 | 114 | /** 115 | * Remap attribute keys 116 | * 117 | * @param array $data 118 | * 119 | * @return array 120 | */ 121 | protected function mapAttributes(array &$data) 122 | { 123 | return $this->getHelper()->mapAttributes($this->attributeMap, $data); 124 | } 125 | 126 | /** 127 | * Reverse remap the attribute keys 128 | * 129 | * @param array $data 130 | * 131 | * @return array 132 | */ 133 | protected function unmapAttributes(array &$data) 134 | { 135 | return $this->getHelper()->unmapAttributes($this->attributeMap, $data); 136 | } 137 | 138 | /** 139 | * Type cast array values 140 | * 141 | * @param array $data 142 | * @param array $typeMap 143 | * 144 | * @return array 145 | * 146 | * @throws Zend_Currency_Exception 147 | * @throws Zend_Locale_Exception 148 | */ 149 | protected function fixTypes(array $data, array $typeMap = []) 150 | { 151 | // This makes me a bit nervous 152 | $currencyCode = $this->loadQuote()->getQuoteCurrencyCode(); 153 | if (empty($currencyCode)) { 154 | $currencyCode = $this->loadQuote()->getStore()->getDefaultCurrencyCode(); 155 | } 156 | 157 | if (empty($typeMap)) { 158 | $typeMap = $this->attributeTypeMap; 159 | } 160 | 161 | foreach ($typeMap as $code => $type) { 162 | if (array_key_exists($code, $data) && (is_scalar($data[$code]) || is_null($data[$code]))) { 163 | switch ($type) { 164 | case 'bool': 165 | $data[$code] = (!empty($data[$code]) && strtolower($data[$code]) !== 'false'); 166 | break; 167 | case 'int': 168 | $data[$code] = intval($data[$code]); 169 | break; 170 | case 'float': 171 | $data[$code] = floatval($data[$code]); 172 | break; 173 | case 'currency': 174 | $amount = floatval($data[$code]); 175 | $precision = Zend_Locale_Data::getContent(null, 'currencyfraction', $currencyCode); 176 | if ($precision === false) { 177 | $precision = Zend_Locale_Data::getContent(null, 'currencyfraction'); 178 | } 179 | if ($precision !== false) { 180 | $amount = round($amount, $precision); 181 | $formatted = Mage::app()->getLocale()->currency($currencyCode)->toCurrency($amount, ['precision' => $precision]); 182 | } else { 183 | $formatted = Mage::app()->getLocale()->currency($currencyCode)->toCurrency($amount); 184 | } 185 | $data[$code] = ['currency' => $currencyCode, 'amount' => $amount, 'formatted' => $formatted]; 186 | break; 187 | case 'string': 188 | default: 189 | $data[$code] = (string)$data[$code]; 190 | break; 191 | } 192 | } 193 | } 194 | 195 | return $data; 196 | } 197 | 198 | /** 199 | * @param false|null|string|string[] $embeds 200 | * 201 | * @return string[] 202 | */ 203 | protected function parseEmbeds($embeds) 204 | { 205 | if ($embeds === false || $embeds === '') { 206 | return []; 207 | } elseif ($embeds === null) { 208 | return $this->defaultEmbeds; 209 | } 210 | 211 | if (is_string($embeds)) { 212 | $embeds = explode(',', $embeds); 213 | } 214 | 215 | if (is_array($embeds)) { 216 | $embeds = array_filter(array_map('trim', $embeds)); 217 | } else { 218 | $embeds = []; 219 | } 220 | 221 | return $embeds; 222 | } 223 | 224 | // @codingStandardsIgnoreStart 225 | protected function __() 226 | { 227 | // @codingStandardsIgnoreEnd 228 | return call_user_func_array([$this->getHelper(), '__'], func_get_args()); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/BillingAddress.php: -------------------------------------------------------------------------------- 1 | 'bool', 19 | 'customer_address_id' => 'int', 20 | ]; 21 | 22 | /** 23 | * Array of external attribute codes that are manually generated 24 | * 25 | * @var string[] 26 | */ 27 | protected $manualAttributes = [ 28 | 'validation_errors', 29 | ]; 30 | 31 | /** 32 | * Dispatch API call 33 | */ 34 | public function dispatch() 35 | { 36 | $address = $this->loadQuote()->getBillingAddress(); 37 | 38 | switch ($this->getActionType() . $this->getOperation()) { 39 | case self::ACTION_TYPE_ENTITY . self::OPERATION_RETRIEVE: 40 | $this->_render($this->prepareResource($address)); 41 | break; 42 | case self::ACTION_TYPE_ENTITY . self::OPERATION_CREATE: 43 | $this->updateResource($address, $this->getRequest()->getBodyParams()); 44 | $this->saveQuote(); 45 | $this->_render($this->prepareResource($address)); 46 | break; 47 | case self::ACTION_TYPE_ENTITY . self::OPERATION_UPDATE: 48 | $this->updateResource($address, $this->getRequest()->getBodyParams()); 49 | $this->saveQuote(); 50 | $this->_render($this->prepareResource($address)); 51 | break; 52 | case self::ACTION_TYPE_ENTITY . self::OPERATION_DELETE: 53 | $address->delete(); 54 | $this->getResponse()->setMimeType($this->getRenderer()->getMimeType()); 55 | $this->getResponse()->setHttpResponseCode(204); 56 | break; 57 | default: 58 | $this->_critical(self::RESOURCE_METHOD_NOT_ALLOWED); 59 | } 60 | } 61 | 62 | /** 63 | * @param Mage_Sales_Model_Quote_Address $resource 64 | * 65 | * @return array 66 | */ 67 | public function prepareResource(Mage_Sales_Model_Quote_Address $resource) 68 | { 69 | // Store current state 70 | $actionType = $this->getActionType(); 71 | $operation = $this->getOperation(); 72 | 73 | // Change state 74 | $this->setActionType(self::ACTION_TYPE_ENTITY); 75 | $this->setOperation(self::OPERATION_RETRIEVE); 76 | 77 | // Get a filter instance 78 | $filter = $this->getFilter(); 79 | 80 | // Get raw outbound data 81 | $data = $this->loadResourceAttributes($resource, $filter->getAttributesToInclude()); 82 | 83 | // ========================= 84 | // BEGIN - Manual attributes 85 | // ========================= 86 | 87 | if (in_array('formatted_html', $filter->getAttributesToInclude())) { 88 | $data['formatted_html'] = $resource->format('html'); 89 | } 90 | 91 | if (in_array('formatted_text', $filter->getAttributesToInclude())) { 92 | $data['formatted_text'] = $resource->format('text'); 93 | } 94 | 95 | if (in_array('validation_errors', $filter->getAttributesToInclude())) { 96 | $data['validation_errors'] = array_filter(array_map('trim', (array)$resource->getData('validation_errors'))); 97 | } 98 | 99 | // ========================= 100 | // END - Manual attributes 101 | // ========================= 102 | 103 | // Fire event 104 | $data = new Varien_Object($data); 105 | Mage::dispatchEvent('aoe_cartapi_billingaddress_prepare', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 106 | $data = $data->getData(); 107 | 108 | // Filter outbound data 109 | $data = $this->getFilter()->out($data); 110 | 111 | // Fix data types 112 | $data = $this->fixTypes($data); 113 | 114 | // Add null values for missing data 115 | foreach ($filter->getAttributesToInclude() as $code) { 116 | if (!array_key_exists($code, $data)) { 117 | $data[$code] = null; 118 | } 119 | } 120 | 121 | // Sort the result by key 122 | ksort($data); 123 | 124 | // Restore old state 125 | $this->setActionType($actionType); 126 | $this->setOperation($operation); 127 | 128 | // Return prepared outbound data 129 | return $data; 130 | } 131 | 132 | /** 133 | * Update the resource model 134 | * 135 | * @param Mage_Sales_Model_Quote_Address $resource 136 | * @param array $data 137 | * 138 | * @return Mage_Sales_Model_Quote_Address 139 | */ 140 | public function updateResource(Mage_Sales_Model_Quote_Address $resource, array $data) 141 | { 142 | // Store current state 143 | $actionType = $this->getActionType(); 144 | $operation = $this->getOperation(); 145 | 146 | // Change state 147 | $this->setActionType(self::ACTION_TYPE_ENTITY); 148 | $this->setOperation(self::OPERATION_UPDATE); 149 | 150 | // Get a filter instance 151 | $filter = $this->getFilter(); 152 | 153 | // Filter raw incoming data 154 | $data = $filter->in($data); 155 | 156 | // Check if the update is setting a customer address ID to use 157 | if (array_key_exists('customer_address_id', $data) && $data['customer_address_id']) { 158 | /** @var Mage_Customer_Model_Address $customerAddress */ 159 | $customerAddress = Mage::getModel('customer/address')->load($data['customer_address_id']); 160 | if ($customerAddress->getId()) { 161 | if ($customerAddress->getCustomerId() != $resource->getQuote()->getCustomerId()) { 162 | $this->_critical(Mage::helper('checkout')->__('Customer Address is not valid.'), Mage_Api2_Model_Server::HTTP_BAD_REQUEST); 163 | } 164 | $resource->importCustomerAddress($customerAddress); 165 | $resource->setSaveInAddressBook(0); 166 | } 167 | } else { 168 | // Fix region/country data 169 | $data = $this->getHelper()->fixAddressData($data, $resource->getCountryId(), $resource->getRegionId()); 170 | 171 | // Get allowed attributes 172 | $allowedAttributes = $filter->getAllowedAttributes(Mage_Api2_Model_Resource::OPERATION_ATTRIBUTE_WRITE); 173 | 174 | // Update model 175 | $this->saveResourceAttributes($resource, array_merge($allowedAttributes, ['region_id']), $data); 176 | } 177 | 178 | // Update the shipping address if it is meant to match the billing address 179 | if ($resource->getQuote()->getShippingAddress()->getSameAsBilling()) { 180 | $shippingAddress = $resource->getQuote()->getShippingAddress(); 181 | $shippingAddress->importCustomerAddress($resource->exportCustomerAddress()); 182 | $shippingAddress->setSameAsBilling(1); 183 | } 184 | 185 | // Validate address 186 | $addressErrors = $this->getHelper()->validateQuoteAddress($resource); 187 | if (!empty($addressErrors)) { 188 | $resource->setData('validation_errors', $addressErrors); 189 | } 190 | 191 | // Fire event - after 192 | $data = new Varien_Object($data); 193 | Mage::dispatchEvent('aoe_cartapi_billingaddress_update_after', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 194 | 195 | // Restore old state 196 | $this->setActionType($actionType); 197 | $this->setOperation($operation); 198 | 199 | // Return updated resource 200 | return $resource; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/ShippingAddress.php: -------------------------------------------------------------------------------- 1 | 'shipping_method', 12 | ]; 13 | 14 | /** 15 | * Hash of external attribute codes and their data type 16 | * 17 | * @var string[] 18 | */ 19 | protected $attributeTypeMap = [ 20 | 'customer_address_id' => 'int', 21 | 'same_as_billing' => 'bool', 22 | 'save_in_address_book' => 'bool', 23 | ]; 24 | 25 | /** 26 | * Array of external attribute codes that are manually generated 27 | * 28 | * @var string[] 29 | */ 30 | protected $manualAttributes = [ 31 | 'validation_errors', 32 | ]; 33 | 34 | /** 35 | * Dispatch API call 36 | */ 37 | public function dispatch() 38 | { 39 | $resource = $this->loadQuote()->getShippingAddress(); 40 | 41 | switch ($this->getActionType() . $this->getOperation()) { 42 | case self::ACTION_TYPE_ENTITY . self::OPERATION_RETRIEVE: 43 | $this->_render($this->prepareResource($resource)); 44 | break; 45 | case self::ACTION_TYPE_ENTITY . self::OPERATION_CREATE: 46 | $this->updateResource($resource, $this->getRequest()->getBodyParams()); 47 | $this->saveQuote(); 48 | $this->_render($this->prepareResource($resource)); 49 | break; 50 | case self::ACTION_TYPE_ENTITY . self::OPERATION_UPDATE: 51 | $this->updateResource($resource, $this->getRequest()->getBodyParams()); 52 | $this->saveQuote(); 53 | $this->_render($this->prepareResource($resource)); 54 | break; 55 | case self::ACTION_TYPE_ENTITY . self::OPERATION_DELETE: 56 | // Grab shipping method code 57 | $shippingMethod = $resource->getShippingMethod(); 58 | $resource->delete()->isDeleted(true); 59 | if ($shippingMethod) { 60 | // Set shipping method code if it was originally set 61 | $this->loadQuote()->getShippingAddress()->setShippingMethod($shippingMethod)->save(); 62 | } 63 | $this->getResponse()->setMimeType($this->getRenderer()->getMimeType()); 64 | $this->getResponse()->setHttpResponseCode(204); 65 | break; 66 | default: 67 | $this->_critical(self::RESOURCE_METHOD_NOT_ALLOWED); 68 | } 69 | } 70 | 71 | /** 72 | * @param Mage_Sales_Model_Quote_Address $resource 73 | * 74 | * @return array 75 | */ 76 | public function prepareResource(Mage_Sales_Model_Quote_Address $resource) 77 | { 78 | // Store current state 79 | $actionType = $this->getActionType(); 80 | $operation = $this->getOperation(); 81 | 82 | // Change state 83 | $this->setActionType(self::ACTION_TYPE_ENTITY); 84 | $this->setOperation(self::OPERATION_RETRIEVE); 85 | 86 | // Get a filter instance 87 | $filter = $this->getFilter(); 88 | 89 | // Get raw outbound data 90 | $data = $this->loadResourceAttributes($resource, $filter->getAttributesToInclude()); 91 | 92 | // ========================= 93 | // BEGIN - Manual attributes 94 | // ========================= 95 | 96 | if (in_array('formatted_html', $filter->getAttributesToInclude())) { 97 | $data['formatted_html'] = $resource->format('html'); 98 | } 99 | 100 | if (in_array('formatted_text', $filter->getAttributesToInclude())) { 101 | $data['formatted_text'] = $resource->format('text'); 102 | } 103 | 104 | if (in_array('validation_errors', $filter->getAttributesToInclude())) { 105 | $data['validation_errors'] = array_filter(array_map('trim', (array)$resource->getData('validation_errors'))); 106 | } 107 | 108 | // ========================= 109 | // END - Manual attributes 110 | // ========================= 111 | 112 | // Fire event 113 | $data = new Varien_Object($data); 114 | Mage::dispatchEvent('aoe_cartapi_shippingaddress_prepare', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 115 | $data = $data->getData(); 116 | 117 | // Filter outbound data 118 | $data = $this->getFilter()->out($data); 119 | 120 | // Fix data types 121 | $data = $this->fixTypes($data); 122 | 123 | // Add null values for missing data 124 | foreach ($filter->getAttributesToInclude() as $code) { 125 | if (!array_key_exists($code, $data)) { 126 | $data[$code] = null; 127 | } 128 | } 129 | 130 | // Sort the result by key 131 | ksort($data); 132 | 133 | // Restore old state 134 | $this->setActionType($actionType); 135 | $this->setOperation($operation); 136 | 137 | // Return prepared outbound data 138 | return $data; 139 | } 140 | 141 | /** 142 | * Update the resource model 143 | * 144 | * @param Mage_Sales_Model_Quote_Address $resource 145 | * @param array $data 146 | * 147 | * @return Mage_Sales_Model_Quote_Address 148 | */ 149 | public function updateResource(Mage_Sales_Model_Quote_Address $resource, array $data) 150 | { 151 | // Store current state 152 | $actionType = $this->getActionType(); 153 | $operation = $this->getOperation(); 154 | 155 | // Change state 156 | $this->setActionType(self::ACTION_TYPE_ENTITY); 157 | $this->setOperation(self::OPERATION_UPDATE); 158 | 159 | // Get a filter instance 160 | $filter = $this->getFilter(); 161 | 162 | // Filter raw incoming data 163 | $data = $filter->in($data); 164 | 165 | // Check if the update is setting a customer address ID to use 166 | if (array_key_exists('customer_address_id', $data) && $data['customer_address_id']) { 167 | /** @var Mage_Customer_Model_Address $customerAddress */ 168 | $customerAddress = Mage::getModel('customer/address')->load($data['customer_address_id']); 169 | if ($customerAddress->getId()) { 170 | if ($customerAddress->getCustomerId() != $resource->getQuote()->getCustomerId()) { 171 | $this->_critical(Mage::helper('checkout')->__('Customer Address is not valid.'), Mage_Api2_Model_Server::HTTP_BAD_REQUEST); 172 | } 173 | $resource->importCustomerAddress($customerAddress); 174 | $resource->setSaveInAddressBook(0); 175 | $resource->setSameAsBilling(0); 176 | } 177 | } elseif (array_key_exists('same_as_billing', $data) && $data['same_as_billing']) { 178 | // Copy data from billing address 179 | $resource->importCustomerAddress($resource->getQuote()->getBillingAddress()->exportCustomerAddress()); 180 | $resource->setSameAsBilling(1); 181 | } else { 182 | // Clear flag 183 | $data['same_as_billing'] = 0; 184 | 185 | // Fix region/country data 186 | $data = $this->getHelper()->fixAddressData($data, $resource->getCountryId(), $resource->getRegionId()); 187 | 188 | // Get allowed attributes 189 | $allowedAttributes = $filter->getAllowedAttributes(Mage_Api2_Model_Resource::OPERATION_ATTRIBUTE_WRITE); 190 | 191 | // Update model 192 | $this->saveResourceAttributes($resource, array_merge($allowedAttributes, ['region_id']), $data); 193 | } 194 | 195 | // Validate address 196 | $addressErrors = $this->getHelper()->validateQuoteAddress($resource); 197 | if (!empty($addressErrors)) { 198 | $resource->setData('validation_errors', $addressErrors); 199 | } 200 | 201 | // Fire event - after 202 | $data = new Varien_Object($data); 203 | Mage::dispatchEvent('aoe_cartapi_shippingaddress_update_after', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 204 | 205 | // Restore old state 206 | $this->setActionType($actionType); 207 | $this->setOperation($operation); 208 | 209 | // Return updated resource 210 | return $resource; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Helper/Data.php: -------------------------------------------------------------------------------- 1 | quote || $forceReload) { 19 | /** @var Mage_Checkout_Model_Session $session */ 20 | $session = Mage::getSingleton('checkout/session'); 21 | $quote = $session->getQuote(); 22 | 23 | Mage::dispatchEvent('aoe_cartapi_load_quote_before', ['quote' => $quote]); 24 | 25 | // Email sync to be compatible with OPC and XMLconnect 26 | if (!is_null($quote->getCustomerEmail()) && is_null($quote->getBillingAddress()->getEmail())) { 27 | // Copy quote email to missing billing email 28 | $quote->getBillingAddress()->setEmail($quote->getCustomerEmail()); 29 | } elseif (is_null($quote->getCustomerEmail()) && !is_null($quote->getBillingAddress()->getEmail())) { 30 | // Copy billing email to missing quote email 31 | $quote->setCustomerEmail($quote->getBillingAddress()->getEmail()); 32 | } elseif (!is_null($quote->getCustomerEmail()) && !is_null($quote->getBillingAddress()->getEmail()) && $quote->getCustomerEmail() !== $quote->getBillingAddress()->getEmail()) { 33 | // Sync quote email to match billing email 34 | $quote->setCustomerEmail($quote->getBillingAddress()->getEmail()); 35 | } 36 | 37 | Mage::dispatchEvent('aoe_cartapi_load_quote_after', ['quote' => $quote]); 38 | 39 | $this->quote = $quote; 40 | } 41 | 42 | return $this->quote; 43 | } 44 | 45 | /** 46 | * @return Mage_Sales_Model_Quote 47 | */ 48 | public function saveQuote() 49 | { 50 | $quote = $this->loadQuote(); 51 | 52 | Mage::dispatchEvent('aoe_cartapi_save_quote_before', ['quote' => $quote]); 53 | 54 | $quote->getBillingAddress(); 55 | 56 | // Email sync to be compatible with OPC and XMLconnect 57 | if ($quote->hasData('customer_email') && !$quote->getBillingAddress()->hasData('email')) { 58 | // Copy quote email to missing billing email 59 | $quote->getBillingAddress()->setEmail($quote->getCustomerEmail()); 60 | } elseif (!$quote->hasData('customer_email') && $quote->getBillingAddress()->hasData('email')) { 61 | // Copy billing email to missing quote email 62 | $quote->setCustomerEmail($quote->getBillingAddress()->getEmail()); 63 | } elseif ($quote->hasData('customer_email') && $quote->getBillingAddress()->hasData('email') && $quote->getCustomerEmail() !== $quote->getBillingAddress()->getEmail()) { 64 | // Sync billing email to match quote email 65 | $quote->getBillingAddress()->setEmail($quote->getCustomerEmail()); 66 | } 67 | 68 | $quote->getShippingAddress()->setCollectShippingRates(true); 69 | 70 | if ($quote->getItemsCount() == 0) { 71 | /** Dirty workaround where the detection of the cache entry where explicit enough THX magento **/ 72 | $quote->getShippingAddress()->unsetData('cached_items_all'); 73 | $quote->getShippingAddress()->unsetData('cached_items_nominal'); 74 | $quote->getShippingAddress()->unsetData('cached_items_nonnominal'); 75 | 76 | /** Force recollection of totals on every run */ 77 | $quote->setTotalsCollectedFlag(false); 78 | } 79 | 80 | $quote->collectTotals(); 81 | 82 | $quote->save(); 83 | 84 | /** @var Mage_Checkout_Model_Session $session */ 85 | $session = Mage::getSingleton('checkout/session'); 86 | $session->setQuoteId($quote->getId()); 87 | 88 | Mage::dispatchEvent('aoe_cartapi_save_quote_after', ['quote' => $quote]); 89 | 90 | return $quote; 91 | } 92 | 93 | /** 94 | * @param Mage_Sales_Model_Quote $quote 95 | * 96 | * @return array[] 97 | */ 98 | public function validateQuote(Mage_Sales_Model_Quote $quote) 99 | { 100 | $errors = []; 101 | 102 | if (!$quote->isVirtual()) { 103 | // Copy data from billing address 104 | if ($quote->getShippingAddress()->getSameAsBilling()) { 105 | $quote->getShippingAddress()->importCustomerAddress($quote->getBillingAddress()->exportCustomerAddress()); 106 | $quote->getShippingAddress()->setSameAsBilling(1); 107 | } 108 | 109 | $addressErrors = $this->validateQuoteAddress($quote->getShippingAddress()); 110 | if (!empty($addressErrors)) { 111 | $errors['shipping_address'] = $addressErrors; 112 | } 113 | 114 | $method = $quote->getShippingAddress()->getShippingMethod(); 115 | $rate = $quote->getShippingAddress()->getShippingRateByCode($method); 116 | if (!$method || !$rate) { 117 | $errors['shipping_method'] = [$this->__('Please specify a valid shipping method.')]; 118 | } 119 | } 120 | 121 | $addressErrors = $this->validateQuoteAddress($quote->getBillingAddress()); 122 | if (!empty($addressErrors)) { 123 | $errors['billing_address'] = $addressErrors; 124 | } 125 | 126 | try { 127 | if (!$quote->getPayment()->getMethod() || !$quote->getPayment()->getMethodInstance()) { 128 | $errors['payment'] = [$this->__('Please select a valid payment method.')]; 129 | } 130 | } catch (Mage_Core_Exception $e) { 131 | $errors['payment'] = [$this->__('Please select a valid payment method.')]; 132 | } 133 | 134 | return $errors; 135 | } 136 | 137 | /** 138 | * @param Mage_Sales_Model_Quote_Address $address 139 | * 140 | * @return string[] 141 | */ 142 | public function validateQuoteAddress(Mage_Sales_Model_Quote_Address $address) 143 | { 144 | /* @var Mage_Customer_Model_Form $addressForm */ 145 | $addressForm = Mage::getModel('customer/form'); 146 | $addressForm->setFormCode('customer_address_edit')->setEntityType('customer_address'); 147 | $addressForm->setEntity($address); 148 | $errors = $addressForm->validateData($address->getData()); 149 | if (!is_array($errors)) { 150 | $errors = $address->validate(); 151 | if (!is_array($errors)) { 152 | $errors = []; 153 | } 154 | } 155 | 156 | return $errors; 157 | } 158 | 159 | /** 160 | * Remap attribute keys 161 | * 162 | * @param array $map 163 | * @param array $data 164 | * 165 | * @return array 166 | */ 167 | public function mapAttributes(array $map, array &$data) 168 | { 169 | $out = []; 170 | 171 | foreach ($data as $key => &$value) { 172 | if (isset($map[$key])) { 173 | $key = $map[$key]; 174 | } 175 | $out[$key] = $value; 176 | } 177 | 178 | return $out; 179 | } 180 | 181 | /** 182 | * Reverse remap the attribute keys 183 | * 184 | * @param array $map 185 | * @param array $data 186 | * 187 | * @return array 188 | */ 189 | public function unmapAttributes(array $map, array &$data) 190 | { 191 | return $this->mapAttributes(array_flip($map), $data); 192 | } 193 | 194 | public function fixAddressData(array $data, $oldCountryId, $oldRegionId) 195 | { 196 | if (array_key_exists('country_id', $data) && !array_key_exists('region', $data)) { 197 | $data['region'] = $oldRegionId; 198 | } 199 | 200 | if (array_key_exists('region', $data)) { 201 | // Clear previous region_id 202 | $data['region_id'] = null; 203 | 204 | // Grab country_id 205 | $countryId = (array_key_exists('country_id', $data) ? $data['country_id'] : $oldCountryId); 206 | 207 | /** @var Mage_Directory_Model_Region $regionModel */ 208 | $regionModel = Mage::getModel('directory/region'); 209 | if (is_numeric($data['region'])) { 210 | $regionModel->load($data['region']); 211 | if ($regionModel->getId() && (empty($countryId) || $regionModel->getCountryId() == $countryId)) { 212 | $data['region'] = $regionModel->getName(); 213 | $data['region_id'] = $regionModel->getId(); 214 | $data['country_id'] = $regionModel->getCountryId(); 215 | } 216 | } elseif (!empty($countryId)) { 217 | $regionModel->loadByCode($data['region'], $countryId); 218 | if (!$regionModel->getId()) { 219 | $regionModel->loadByName($data['region'], $countryId); 220 | } 221 | if ($regionModel->getId()) { 222 | $data['region'] = $regionModel->getName(); 223 | $data['region_id'] = $regionModel->getId(); 224 | } 225 | } 226 | } 227 | 228 | return $data; 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/Crosssell.php: -------------------------------------------------------------------------------- 1 | 'url_in_store', 14 | ]; 15 | 16 | /** 17 | * Hash of external attribute codes and their data type 18 | * 19 | * @var string[] 20 | */ 21 | protected $attributeTypeMap = [ 22 | 'sku' => 'string', 23 | 'name' => 'string', 24 | 'description' => 'string', 25 | 'short_description' => 'string', 26 | 'url' => 'string', 27 | 'is_saleable' => 'bool', 28 | 'is_in_stock' => 'bool', 29 | 'qty' => 'float', 30 | 'min_sale_qty' => 'float', 31 | 'max_sale_qty' => 'float', 32 | 'price' => 'currency', 33 | 'minimal_price' => 'currency', 34 | 'final_price' => 'currency', 35 | ]; 36 | 37 | /** 38 | * Array of external attribute codes that are manually generated 39 | * 40 | * @var string[] 41 | */ 42 | protected $manualAttributes = [ 43 | 'is_saleable', 44 | 'is_in_stock', 45 | 'qty', 46 | 'min_sale_qty', 47 | 'max_sale_qty', 48 | 'images', 49 | ]; 50 | 51 | /** 52 | * Dispatch API call 53 | */ 54 | public function dispatch() 55 | { 56 | $quote = $this->loadQuote(); 57 | 58 | switch ($this->getActionType() . $this->getOperation()) { 59 | case self::ACTION_TYPE_COLLECTION . self::OPERATION_RETRIEVE: 60 | $this->_render($this->prepareCollection($quote)); 61 | break; 62 | default: 63 | $this->_critical(self::RESOURCE_METHOD_NOT_ALLOWED); 64 | } 65 | } 66 | 67 | /** 68 | * Convert the resource model collection to an array 69 | * 70 | * @param Mage_Sales_Model_Quote $quote 71 | * 72 | * @return array 73 | */ 74 | public function prepareCollection(Mage_Sales_Model_Quote $quote, $applyCollectionModifiers = true) 75 | { 76 | // This collection should always be a key/value hash and never a simple array 77 | $data = new ArrayObject(); 78 | 79 | if ($quote->isVirtual()) { 80 | return $data; 81 | } 82 | 83 | // Store current state 84 | $actionType = $this->getActionType(); 85 | $operation = $this->getOperation(); 86 | 87 | // Change state 88 | $this->setActionType(self::ACTION_TYPE_COLLECTION); 89 | $this->setOperation(self::OPERATION_RETRIEVE); 90 | 91 | // Get filter 92 | $filter = $this->getFilter(); 93 | 94 | // Prepare collection 95 | foreach ($this->getCrosssellProducts($quote, $applyCollectionModifiers) as $product) { 96 | $data[$product->getSku()] = $this->prepareProduct($product, $filter); 97 | } 98 | 99 | // Restore old state 100 | $this->setActionType($actionType); 101 | $this->setOperation($operation); 102 | 103 | // Return prepared outbound data 104 | return $data; 105 | } 106 | 107 | protected function prepareProduct(Mage_Catalog_Model_Product $product, Mage_Api2_Model_Acl_Filter $filter) 108 | { 109 | // Get raw outbound data 110 | $data = $this->loadResourceAttributes($product, $filter->getAttributesToInclude()); 111 | 112 | // ========================= 113 | // BEGIN - Manual attributes 114 | // ========================= 115 | 116 | // Add stock data 117 | if (in_array('is_saleable', $filter->getAttributesToInclude())) { 118 | $data['is_saleable'] = $product->isSaleable(); 119 | } 120 | $stockItem = $product->getStockItem(); 121 | if ($stockItem instanceof Mage_CatalogInventory_Model_Stock_Item) { 122 | if (in_array('is_in_stock', $filter->getAttributesToInclude())) { 123 | $data['is_in_stock'] = $stockItem->getIsInStock(); 124 | } 125 | if (in_array('qty', $filter->getAttributesToInclude())) { 126 | $data['qty'] = $stockItem->getQty(); 127 | } 128 | if (in_array('min_sale_qty', $filter->getAttributesToInclude())) { 129 | $data['min_sale_qty'] = $stockItem->getMinSaleQty(); 130 | } 131 | if (in_array('max_sale_qty', $filter->getAttributesToInclude())) { 132 | $data['max_sale_qty'] = $stockItem->getMaxSaleQty(); 133 | } 134 | } 135 | 136 | // Add images 137 | if (in_array('images', $filter->getAttributesToInclude())) { 138 | $data['images'] = $this->getImageUrls($product); 139 | } 140 | 141 | // ========================= 142 | // END - Manual attributes 143 | // ========================= 144 | 145 | // Fire event 146 | $data = new Varien_Object($data); 147 | Mage::dispatchEvent('aoe_cartapi_crosssell_prepare', ['data' => $data, 'filter' => $filter, 'resource' => $product]); 148 | $data = $data->getData(); 149 | 150 | // Filter outbound data 151 | $data = $filter->out($data); 152 | 153 | // Fix data types 154 | $data = $this->fixTypes($data); 155 | 156 | // Add null values for missing data 157 | foreach ($filter->getAttributesToInclude() as $code) { 158 | if (!array_key_exists($code, $data)) { 159 | $data[$code] = null; 160 | } 161 | } 162 | 163 | // Sort the result by key 164 | ksort($data); 165 | 166 | return $data; 167 | } 168 | 169 | /** 170 | * @param Mage_Sales_Model_Quote $quote 171 | * @param bool $applyCollectionModifiers 172 | * 173 | * @return Mage_Catalog_Model_Product[] 174 | */ 175 | protected function getCrosssellProducts(Mage_Sales_Model_Quote $quote, $applyCollectionModifiers = true) 176 | { 177 | $cartProductIds = []; 178 | foreach ($quote->getAllItems() as $item) { 179 | /** @var Mage_Sales_Model_Quote_Item $item */ 180 | if ($product = $item->getProduct()) { 181 | $cartProductIds[] = intval($product->getId()); 182 | } 183 | } 184 | 185 | if ($cartProductIds) { 186 | /** @var Mage_Catalog_Model_Resource_Product_Link_Product_Collection $collection */ 187 | $collection = Mage::getModel('catalog/product_link')->useCrossSellLinks()->getProductCollection(); 188 | $collection->setStoreId($quote->getStoreId()); 189 | $collection->addStoreFilter(); 190 | //$collection->addAttributeToSelect(Mage::getSingleton('catalog/config')->getProductAttributes()); 191 | $collection->addAttributeToSelect('*'); 192 | $collection->addUrlRewrite(); 193 | $collection->addAttributeToFilter('status', ['eq' => Mage_Catalog_Model_Product_Status::STATUS_ENABLED]); 194 | Mage::getSingleton('catalog/product_visibility')->addVisibleInSiteFilterToCollection($collection); 195 | Mage::getSingleton('cataloginventory/stock')->addInStockFilterToCollection($collection); 196 | $collection->applyFrontendPriceLimitations(); 197 | 198 | $collection->addProductFilter($cartProductIds); 199 | $collection->addExcludeProductFilter($cartProductIds); 200 | $collection->setGroupBy(); 201 | $collection->setPositionOrder(); 202 | 203 | if ($applyCollectionModifiers) { 204 | $this->_applyCollectionModifiers($collection); 205 | } 206 | 207 | /** @var Mage_CatalogInventory_Model_Stock $stock */ 208 | $stock = Mage::getModel('cataloginventory/stock'); 209 | $stock->addItemsToProducts($collection); 210 | 211 | return $collection->getItems(); 212 | } else { 213 | return []; 214 | } 215 | } 216 | 217 | protected function getImageUrls(Mage_Catalog_Model_Product $product) 218 | { 219 | $data = []; 220 | 221 | /** @var Mage_Catalog_Helper_Image $helper */ 222 | $helper = Mage::helper('catalog/image'); 223 | 224 | // Add normal URL 225 | $helper->init($product, 'image'); 226 | $size = Mage::getStoreConfig(Mage_Catalog_Helper_Image::XML_NODE_PRODUCT_BASE_IMAGE_WIDTH); 227 | if (is_numeric($size)) { 228 | $helper->constrainOnly(true)->resize($size); 229 | } 230 | $data['normal'] = $helper->__toString(); 231 | 232 | // Add small URL 233 | $helper->init($product, 'small_image'); 234 | $size = Mage::getStoreConfig(Mage_Catalog_Helper_Image::XML_NODE_PRODUCT_SMALL_IMAGE_WIDTH); 235 | if (is_numeric($size)) { 236 | $helper->constrainOnly(true)->resize($size); 237 | } 238 | $data['small'] = $helper->__toString(); 239 | 240 | // Add thumbnail URL 241 | $helper->init($product, 'thumbnail'); 242 | $size = Mage::getStoreConfig('catalog/product_image/thumbnail_width'); 243 | if (is_numeric($size)) { 244 | $helper->constrainOnly(true)->resize($size); 245 | } 246 | $data['thumbnail'] = $helper->__toString(); 247 | 248 | return $data; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Open Software License ("OSL") v. 3.0 3 | 4 | This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: 5 | 6 | Licensed under the Open Software License version 3.0 7 | 8 | 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: 9 | 10 | 1. to reproduce the Original Work in copies, either alone or as part of a collective work; 11 | 12 | 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; 13 | 14 | 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; 15 | 16 | 4. to perform the Original Work publicly; and 17 | 18 | 5. to display the Original Work publicly. 19 | 20 | 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. 21 | 22 | 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. 23 | 24 | 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. 25 | 26 | 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). 27 | 28 | 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. 29 | 30 | 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. 31 | 32 | 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. 33 | 34 | 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). 35 | 36 | 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. 37 | 38 | 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. 39 | 40 | 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. 41 | 42 | 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. 43 | 44 | 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 45 | 46 | 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. 47 | 48 | 16. Modification of This License. This License is Copyright � 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under " or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. 49 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/Cart.php: -------------------------------------------------------------------------------- 1 | 'customer_email', 12 | ]; 13 | 14 | /** 15 | * Hash of external attribute codes and their data type 16 | * 17 | * @var string[] 18 | */ 19 | protected $attributeTypeMap = [ 20 | 'email' => 'string', 21 | 'coupon_code' => 'string', 22 | 'has_error' => 'bool', 23 | 'shipping_method' => 'string', 24 | 'qty' => 'float', 25 | ]; 26 | 27 | /** 28 | * Array of external attribute codes that are manually generated 29 | * 30 | * @var string[] 31 | */ 32 | protected $manualAttributes = [ 33 | 'shipping_method', 34 | 'qty', 35 | 'totals', 36 | 'messages', 37 | ]; 38 | 39 | /** 40 | * Dispatch API call 41 | */ 42 | public function dispatch() 43 | { 44 | $quote = $this->loadQuote(); 45 | 46 | switch ($this->getActionType() . $this->getOperation()) { 47 | case self::ACTION_TYPE_ENTITY . self::OPERATION_RETRIEVE: 48 | $this->_render($this->prepareResource($quote)); 49 | break; 50 | case self::ACTION_TYPE_ENTITY . self::OPERATION_CREATE: 51 | $this->updateResource($quote, $this->getRequest()->getBodyParams()); 52 | $this->saveQuote(); 53 | $this->_render($this->prepareResource($quote)); 54 | break; 55 | case self::ACTION_TYPE_ENTITY . self::OPERATION_UPDATE: 56 | $this->updateResource($quote, $this->getRequest()->getBodyParams()); 57 | $this->saveQuote(); 58 | $this->_render($this->prepareResource($quote)); 59 | break; 60 | case self::ACTION_TYPE_ENTITY . self::OPERATION_DELETE: 61 | if ($quote->getId()) { 62 | $quote->setIsActive(false); 63 | $this->saveQuote(); 64 | //$quote->delete(); 65 | } 66 | $this->getResponse()->setMimeType($this->getRenderer()->getMimeType()); 67 | $this->getResponse()->setHttpResponseCode(204); 68 | break; 69 | default: 70 | $this->_critical(self::RESOURCE_METHOD_NOT_ALLOWED); 71 | } 72 | } 73 | 74 | /** 75 | * @param Mage_Sales_Model_Quote $resource 76 | * 77 | * @return array 78 | */ 79 | public function prepareResource(Mage_Sales_Model_Quote $resource) 80 | { 81 | // Store current state 82 | $actionType = $this->getActionType(); 83 | $operation = $this->getOperation(); 84 | 85 | // Change state 86 | $this->setActionType(self::ACTION_TYPE_ENTITY); 87 | $this->setOperation(self::OPERATION_RETRIEVE); 88 | 89 | // Get a filter instance 90 | $filter = $this->getFilter(); 91 | 92 | // Get the embeds list 93 | $embeds = $this->parseEmbeds($this->getRequest()->getParam('embed')); 94 | 95 | // Check for the validation embed and validate the quote if needed 96 | if (in_array('validation', $embeds)) { 97 | $errors = $this->getHelper()->validateQuote($resource); 98 | $resource->setData('__validation_errors__', $errors); 99 | // Save the validation results (since address normalization happens here) 100 | $this->saveQuote(); 101 | } 102 | 103 | // Get raw outbound data 104 | $data = $this->loadResourceAttributes($resource, $filter->getAttributesToInclude()); 105 | 106 | // ========================= 107 | // BEGIN - Manual attributes 108 | // ========================= 109 | 110 | // Shipping method 111 | if (in_array('shipping_method', $filter->getAttributesToInclude())) { 112 | $data['shipping_method'] = $resource->getShippingAddress()->getShippingMethod(); 113 | } 114 | 115 | // Cart qty summary 116 | if (in_array('qty', $filter->getAttributesToInclude())) { 117 | $data['qty'] = (Mage::getStoreConfig('checkout/cart_link/use_qty') ? $resource->getItemsQty() : $resource->getItemsCount()); 118 | } 119 | 120 | // Add in totals 121 | if (in_array('totals', $filter->getAttributesToInclude())) { 122 | $totalsValues = []; 123 | $totalsTypeMap = []; 124 | $totalsTitles = []; 125 | foreach ($resource->getTotals() as $code => $total) { 126 | /* @var Mage_Sales_Model_Quote_Address_Total_Abstract $total */ 127 | $totalsValues[$code] = $total->getValue(); 128 | $totalsTypeMap[$code] = 'currency'; 129 | $totalsTitles[$code] = $total->getTitle(); 130 | } 131 | $data['totals'] = $this->fixTypes($totalsValues, $totalsTypeMap); 132 | foreach ($data['totals'] as $code => $total) { 133 | $data['totals'][$code]['title'] = $totalsTitles[$code]; 134 | } 135 | } 136 | 137 | // Add in validation/error messages 138 | if (in_array('messages', $filter->getAttributesToInclude())) { 139 | $data['messages'] = new ArrayObject(); 140 | foreach ($resource->getMessages() as $message) { 141 | /** @var Mage_Core_Model_Message_Abstract $message */ 142 | $data['messages'][$message->getType()][] = $message->getText(); 143 | } 144 | } 145 | 146 | // ========================= 147 | // END - Manual attributes 148 | // ========================= 149 | 150 | // Fire event 151 | $data = new Varien_Object($data); 152 | Mage::dispatchEvent('aoe_cartapi_cart_prepare', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 153 | $data = $data->getData(); 154 | 155 | // Filter outbound data 156 | $data = $filter->out($data); 157 | 158 | // Handle embeds - This happens after output filtering on purpose 159 | foreach ($embeds as $embed) { 160 | switch ($embed) { 161 | case 'items': 162 | if ($this->_isSubCallAllowed('aoe_cartapi_item')) { 163 | /** @var Aoe_CartApi_Model_Item $subModel */ 164 | $subModel = $this->_getSubModel('aoe_cartapi_item', ['embed' => false]); 165 | $data['items'] = $subModel->prepareCollection($resource); 166 | } 167 | break; 168 | case 'billing_address': 169 | if ($this->_isSubCallAllowed('aoe_cartapi_billing_address')) { 170 | /** @var Aoe_CartApi_Model_BillingAddress $subModel */ 171 | $subModel = $this->_getSubModel('aoe_cartapi_billing_address', []); 172 | $data['billing_address'] = $subModel->prepareResource($resource->getBillingAddress()); 173 | } 174 | break; 175 | case 'shipping_address': 176 | if ($this->_isSubCallAllowed('aoe_cartapi_shipping_address')) { 177 | /** @var Aoe_CartApi_Model_ShippingAddress $subModel */ 178 | $subModel = $this->_getSubModel('aoe_cartapi_shipping_address', []); 179 | $data['shipping_address'] = $subModel->prepareResource($resource->getShippingAddress()); 180 | } 181 | break; 182 | case 'shipping_methods': 183 | if ($this->_isSubCallAllowed('aoe_cartapi_shipping_method')) { 184 | /** @var Aoe_CartApi_Model_ShippingMethod $subModel */ 185 | $subModel = $this->_getSubModel('aoe_cartapi_shipping_method', ['embed' => false]); 186 | $data['shipping_methods'] = $subModel->prepareCollection($resource); 187 | } 188 | break; 189 | case 'crosssells': 190 | if ($this->_isSubCallAllowed('aoe_cartapi_crosssell')) { 191 | /** @var Aoe_CartApi_Model_Crosssell $subModel */ 192 | $subModel = $this->_getSubModel('aoe_cartapi_crosssell', ['embed' => false]); 193 | $data['crosssells'] = $subModel->prepareCollection($resource); 194 | } 195 | break; 196 | case 'payment': 197 | if ($this->_isSubCallAllowed('aoe_cartapi_payment')) { 198 | /** @var Aoe_CartApi_Model_Payment $subModel */ 199 | $subModel = $this->_getSubModel('aoe_cartapi_payment', ['embed' => false]); 200 | $data['payment'] = $subModel->prepareResource($resource->getPayment()); 201 | } 202 | break; 203 | case 'payment_methods': 204 | if ($this->_isSubCallAllowed('aoe_cartapi_payment_methods')) { 205 | $subModel = $this->_getSubModel('aoe_cartapi_payment_methods', ['embed' => false]); 206 | $data['payment_methods'] = $subModel->prepareCollection($resource); 207 | } 208 | break; 209 | case 'validation': 210 | $validationErrors = $resource->getData('__validation_errors__'); 211 | $data['validation'] = (is_array($validationErrors) ? $validationErrors : []); 212 | break; 213 | } 214 | } 215 | 216 | // Fix data types 217 | $data = $this->fixTypes($data); 218 | 219 | // Add null values for missing data 220 | foreach ($filter->getAttributesToInclude() as $code) { 221 | if (!array_key_exists($code, $data)) { 222 | $data[$code] = null; 223 | } 224 | } 225 | 226 | // Sort the result by key 227 | ksort($data); 228 | 229 | // Restore old state 230 | $this->setActionType($actionType); 231 | $this->setOperation($operation); 232 | 233 | // Return prepared outbound data 234 | return $data; 235 | } 236 | 237 | /** 238 | * Update the resource model 239 | * 240 | * @param Mage_Sales_Model_Quote $resource 241 | * @param array $data 242 | * 243 | * @return Mage_Sales_Model_Quote 244 | */ 245 | public function updateResource(Mage_Sales_Model_Quote $resource, array $data) 246 | { 247 | // Store current state 248 | $actionType = $this->getActionType(); 249 | $operation = $this->getOperation(); 250 | 251 | // Change state 252 | $this->setActionType(self::ACTION_TYPE_ENTITY); 253 | $this->setOperation(self::OPERATION_UPDATE); 254 | 255 | // Get a filter instance 256 | $filter = $this->getFilter(); 257 | 258 | // Fire event - before filter 259 | $data = new Varien_Object($data); 260 | Mage::dispatchEvent('aoe_cartapi_cart_update_prefilter', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 261 | $data = $data->getData(); 262 | 263 | // Get allowed attributes 264 | $allowedAttributes = $filter->getAllowedAttributes(Mage_Api2_Model_Resource::OPERATION_ATTRIBUTE_WRITE); 265 | 266 | // Update model 267 | $this->saveResourceAttributes($resource, $allowedAttributes, $data); 268 | 269 | // ========================= 270 | // BEGIN - Manual attributes 271 | // ========================= 272 | 273 | // Shipping Method 274 | if (in_array('shipping_method', $allowedAttributes) && array_key_exists('shipping_method', $data)) { 275 | $resource->getShippingAddress()->setShippingMethod($data['shipping_method']); 276 | } 277 | 278 | // ========================= 279 | // END - Manual attributes 280 | // ========================= 281 | 282 | // Handle embeds - This is a subset of possible embeds 283 | foreach ($this->parseEmbeds($this->getRequest()->getParam('embed')) as $embed) { 284 | switch ($embed) { 285 | case 'billing_address': 286 | if (array_key_exists('billing_address', $data) && $this->_isSubCallAllowed('aoe_cartapi_billing_address')) { 287 | /** @var Aoe_CartApi_Model_BillingAddress $subModel */ 288 | $subModel = $this->_getSubModel('aoe_cartapi_billing_address', []); 289 | $subModel->updateResource($resource->getBillingAddress(), $data['billing_address']); 290 | } 291 | break; 292 | case 'shipping_address': 293 | if (array_key_exists('shipping_address', $data) && $this->_isSubCallAllowed('aoe_cartapi_shipping_address')) { 294 | /** @var Aoe_CartApi_Model_ShippingAddress $subModel */ 295 | $subModel = $this->_getSubModel('aoe_cartapi_shipping_address', []); 296 | $subModel->updateResource($resource->getShippingAddress(), $data['shipping_address']); 297 | } 298 | break; 299 | case 'payment': 300 | if (array_key_exists('payment', $data) && $this->_isSubCallAllowed('aoe_cartapi_payment')) { 301 | /** @var Aoe_CartApi_Model_Payment $subModel */ 302 | $subModel = $this->_getSubModel('aoe_cartapi_payment', []); 303 | $subModel->updateResource($resource->getPayment(), $data['payment']); 304 | } 305 | break; 306 | case 'payment_methods': 307 | if ($this->_isSubCallAllowed('aoe_cartapi_payment_methods')) { 308 | $subModel = $this->_getSubModel('aoe_cartapi_payment_methods', []); 309 | $subModel->updateResource($resource->getPayment(), $data['payment_methods']); 310 | } 311 | break; 312 | } 313 | } 314 | 315 | // Fire event 316 | $data = new Varien_Object($data); 317 | Mage::dispatchEvent('aoe_cartapi_cart_update', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 318 | $data = $data->getData(); 319 | 320 | $failedValidation = false; 321 | 322 | $resource->collectTotals(); 323 | if (isset($data['coupon_code']) && $resource->getCouponCode() != $data['coupon_code']) { 324 | $failedValidation = true; 325 | $message = Mage::helper('checkout')->__('Coupon code "%s" is not valid.', $data['coupon_code']); 326 | $this->getResponse()->setException(new Mage_Api2_Exception($message, Mage_Api2_Model_Server::HTTP_BAD_REQUEST)); 327 | } 328 | 329 | if ($failedValidation) { 330 | $this->_critical('Failed validation', Mage_Api2_Model_Server::HTTP_BAD_REQUEST); 331 | } 332 | 333 | // Restore old state 334 | $this->setActionType($actionType); 335 | $this->setOperation($operation); 336 | 337 | // Return updated resource 338 | return $resource; 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![AOE](aoe-logo.png)](http://www.aoe.com) 2 | 3 | # Aoe_CartApi Magento Module [![Build Status](https://travis-ci.org/AOEpeople/Aoe_CartApi.svg?branch=master)](https://travis-ci.org/AOEpeople/Aoe_CartApi) 4 | 5 | **NOTE**: This module is NOT ready for public consuption. Once it is ready we will tag a 1.0.0 version. 6 | 7 | **NOTE**: Following "documentation" is just a dump of some notes while planning this API. 8 | 9 | ## Primary cart API endpoints 10 | 11 | ### GET /api/rest/cart 12 | Return the cart (quote) for the current frontend Magento session 13 | 14 | { 15 | "email": "fake@example.com", 16 | "coupon_code": "", 17 | "shipping_method": "flatrate_flatrate", 18 | "qty": 5, 19 | "totals": { 20 | "subtotal": { 21 | "title": "Subtotal", 22 | "formatted": "$0.00", 23 | "amount": 0, 24 | "currency": "USD" 25 | }, 26 | "shipping": { 27 | "title": "Shipping", 28 | "formatted": "$0.00", 29 | "amount": 0, 30 | "currency": "USD" 31 | }, 32 | "discount": { 33 | "title": "Discount" 34 | "formatted": "$0.00", 35 | "amount": 0, 36 | "currency": "USD" 37 | }, 38 | "tax": { 39 | "title": "Tax", 40 | "formatted": "$0.00", 41 | "amount": 0, 42 | "currency": "USD" 43 | }, 44 | "grand_total": { 45 | "title": "Grand Total", 46 | "formatted": "$0.00", 47 | "amount": 0, 48 | "currency": "USD" 49 | } 50 | }, 51 | "messages": { 52 | "error": [ 53 | "error message #1", 54 | "error message #2" 55 | ] 56 | "notice": [ 57 | "notice message #1", 58 | "notice message #2" 59 | ] 60 | "success": [ 61 | "success message #1", 62 | "success message #2" 63 | ] 64 | }, 65 | "has_error": false 66 | } 67 | 68 | Supported query parameters 69 | 70 | * attrs 71 | * comma separated list of resource attributes you want returned 72 | * email 73 | * coupon_code 74 | * shipping_method 75 | * qty 76 | * totals 77 | * messages 78 | * has_error 79 | * embed 80 | * comma separated list of sub-resources to embed 81 | * items 82 | * billing_address 83 | * shipping_address 84 | * shipping_methods 85 | * payment_methods (R) 86 | 87 | 88 | ### POST /api/rest/cart 89 | Update attributes of the cart resource. Using the 'embed' query parameter will allow updating of a limited subset of sub-resources as well. 90 | 91 | { 92 | "coupon_code": "FREESTUFF" 93 | } 94 | 95 | Supported query parameters 96 | 97 | * attrs 98 | * comma separated list of resource attributes you want returned 99 | * email 100 | * coupon_code 101 | * shipping_method 102 | * qty 103 | * totals 104 | * messages 105 | * has_error 106 | * embed 107 | * comma separated list of sub-resources to embed (R) and possibly update (W) 108 | * items (R) 109 | * billing_address (R/W) 110 | * shipping_address (R/W) 111 | * shipping_methods (R) 112 | * payment_methods (R) 113 | 114 | ### DELETE /api/rest/cart 115 | Reset the cart and all sub-resources 116 | 117 | ### GET /api/rest/cart/items 118 | Get collection of cart items. This will always return a JS object as result, even if the collection is empty. 119 | 120 | { 121 | "139303": { 122 | "item_id": 139303, 123 | "sku": "ABC123", 124 | "name": "Thing #1", 125 | "images": { 126 | "normal": "", 127 | "small": "", 128 | "thumbnail": "", 129 | }, 130 | "children": [] 131 | "qty": 5, 132 | "backorder_qty": 5, 133 | "original_price": { 134 | "formatted": "$0.00", 135 | "amount": 0, 136 | "currency": "USD" 137 | }, 138 | "price": { 139 | "formatted": "$0.00", 140 | "amount": 0, 141 | "currency": "USD" 142 | }, 143 | "row_total": { 144 | "formatted": "$0.00", 145 | "amount": 0, 146 | "currency": "USD" 147 | }, 148 | "error_info": {}, 149 | "is_saleable": true 150 | } 151 | } 152 | 153 | Supported query parameters 154 | 155 | * attrs 156 | * comma separated list of resource attributes you want returned 157 | * item_id 158 | * sku 159 | * name 160 | * images 161 | * children 162 | * qty 163 | * backorder_qty 164 | * original_price 165 | * price 166 | * row_total 167 | * messages 168 | * error_info 169 | * is_saleable 170 | 171 | ### POST /api/rest/cart/items 172 | Add an product to the cart. This will re-use existing items in the cart if possible. The qty attribute is optional and will default to a single unit. 173 | 174 | { 175 | "sku": "ABC123" 176 | "qty": 1 177 | } 178 | 179 | You can also post multiple items at once using the *items* parameter. If you are using *items* then *sku* and *qty* will not have any effect. 180 | 181 | { 182 | "items": [ 183 | {sku:"ABC123", qty:"2"}, 184 | {sku:"ABC456", qty:"4"} 185 | } 186 | } 187 | 188 | Supported query parameters 189 | 190 | * attrs 191 | * comma separated list of resource attributes you want returned 192 | * item_id 193 | * sku 194 | * name 195 | * images 196 | * children 197 | * qty 198 | * backorder_qty 199 | * original_price 200 | * price 201 | * row_total 202 | * messages 203 | * error_info 204 | * is_saleable 205 | 206 | ### DELETE /api/rest/cart/items 207 | Remove all items from the cart 208 | 209 | ### GET /api/rest/cart/items/:item_id 210 | Get a specific cart item 211 | 212 | { 213 | "item_id": 139303, 214 | "sku": "ABC123", 215 | "name": "Thing #1", 216 | "images": { 217 | "normal": "", 218 | "small": "", 219 | "thumbnail": "", 220 | }, 221 | "children": [] 222 | "qty": 5, 223 | "backorder_qty": 5, 224 | "original_price": { 225 | "formatted": "$0.00", 226 | "amount": 0, 227 | "currency": "USD" 228 | }, 229 | "price": { 230 | "formatted": "$0.00", 231 | "amount": 0, 232 | "currency": "USD" 233 | }, 234 | "row_total": { 235 | "formatted": "$0.00", 236 | "amount": 0, 237 | "currency": "USD" 238 | }, 239 | "error_info": {}, 240 | "is_saleable": true 241 | } 242 | 243 | Supported query parameters 244 | 245 | * attrs 246 | * comma separated list of resource attributes you want returned 247 | * item_id 248 | * sku 249 | * name 250 | * images 251 | * children 252 | * qty 253 | * backorder_qty 254 | * original_price 255 | * price 256 | * row_total 257 | * messages 258 | * error_info 259 | * is_saleable 260 | 261 | ### PUT/POST /api/rest/cart/items/:item_id 262 | Update the quantity for an item in the cart 263 | 264 | { 265 | "qty": 4 266 | } 267 | 268 | Supported query parameters 269 | 270 | * attrs 271 | * comma separated list of resource attributes you want returned 272 | * item_id 273 | * sku 274 | * name 275 | * images 276 | * children 277 | * qty 278 | * backorder_qty 279 | * original_price 280 | * price 281 | * row_total 282 | * messages 283 | * error_info 284 | * is_saleable 285 | 286 | ### DELETE /api/rest/cart/items/:item_id 287 | Remove an item from the cart 288 | 289 | ### GET /api/rest/cart/billing_address 290 | Return the billing address linked to the cart 291 | 292 | { 293 | "firstname": "John", 294 | "middlename": "Quincy", 295 | "lastname": "Public", 296 | "prefix": "Mr.", 297 | "suffix": "Jr.", 298 | "company": "Acme Inc.", 299 | "street":[ 300 | "Street 1", 301 | "Street 2" 302 | ], 303 | "city": "Burlingame", 304 | "region": "California", 305 | "postcode": "00000", 306 | "country_id": "US", 307 | "telephone": "000-000-0000", 308 | "validation_errors":[ 309 | "Error Text", 310 | "Error Text" 311 | ] 312 | } 313 | 314 | Supported query parameters 315 | 316 | * attrs 317 | * comma separated list of resource attributes you want returned 318 | * firstname 319 | * middlename 320 | * lastname 321 | * prefix 322 | * suffix 323 | * company 324 | * street 325 | * city 326 | * region 327 | * postcode 328 | * country_id 329 | * telephone 330 | * formatted_html 331 | * formatted_text 332 | * validation_errors - This will **only** be populated in response to a PUT/POST 333 | 334 | ### PUT/POST /api/rest/cart/billing_address 335 | Update the billing address. All attributes are optional. 336 | 337 | Regions are a bit of 'magic'. 338 | You can send the Mage_Directory DB ID, The region 'code', or the region 'name'. 339 | The code and name are looked up in relation to the currently selected country. 340 | The stored region is either the valid region name (looked up by ID/Code/Name) or the value sent as-is. 341 | 342 | { 343 | "region": "FL" 344 | } 345 | 346 | Supported query parameters 347 | 348 | * attrs 349 | * comma separated list of resource attributes you want returned 350 | * firstname 351 | * middlename 352 | * lastname 353 | * prefix 354 | * suffix 355 | * company 356 | * street 357 | * city 358 | * region 359 | * postcode 360 | * country_id 361 | * telephone 362 | * formatted_html 363 | * formatted_text 364 | * validation_errors - This will **only** be populated in response to a PUT/POST 365 | 366 | ### DELETE /api/rest/cart/billing_address 367 | Reset the billing address 368 | 369 | ### GET /api/rest/cart/shipping_address 370 | Return the shipping address linked to the cart 371 | 372 | { 373 | "firstname": "John", 374 | "middlename": "Quincy", 375 | "lastname": "Public", 376 | "prefix": "Mr.", 377 | "suffix": "Jr.", 378 | "company": "Acme Inc.", 379 | "street":[ 380 | "Street 1", 381 | "Street 2" 382 | ], 383 | "city": "Burlingame", 384 | "region": "California", 385 | "postcode": "00000", 386 | "country_id": "US", 387 | "telephone": "000-000-0000", 388 | "validation_errors":[ 389 | "Error Text", 390 | "Error Text" 391 | ] 392 | } 393 | 394 | Supported query parameters 395 | 396 | * attrs 397 | * comma separated list of resource attributes you want returned 398 | * firstname 399 | * middlename 400 | * lastname 401 | * prefix 402 | * suffix 403 | * company 404 | * street 405 | * city 406 | * region 407 | * postcode 408 | * country_id 409 | * telephone 410 | * formatted_html 411 | * formatted_text 412 | * validation_errors - This will **only** be populated in response to a PUT/POST 413 | 414 | ### PUT/POST /api/rest/cart/shipping_address 415 | Update the shipping address All attributes are optional. 416 | 417 | Regions are a bit of 'magic'. 418 | You can send the Mage_Directory DB ID, The region 'code', or the region 'name'. 419 | The code and name are looked up in relation to the currently selected country. 420 | The stored region is either the valid region name (looked up by ID/Code/Name) or the value sent as-is. 421 | 422 | { 423 | "region": "FL" 424 | } 425 | 426 | Supported query parameters 427 | 428 | * attrs 429 | * comma separated list of resource attributes you want returned 430 | * firstname 431 | * middlename 432 | * lastname 433 | * prefix 434 | * suffix 435 | * company 436 | * street 437 | * city 438 | * region 439 | * postcode 440 | * country_id 441 | * telephone 442 | * formatted_html 443 | * formatted_text 444 | * validation_errors - This will **only** be populated in response to a PUT/POST 445 | 446 | ### DELETE /api/rest/cart/shipping_address 447 | Reset the shipping address 448 | 449 | ## Additional cart related resources 450 | 451 | ### GET /api/rest/cart/crosssells 452 | Return a collection of crosssell products. 453 | **NOTE**: This collection changes as the cart data changes. 454 | 455 | { 456 | "000-000": { 457 | "sku": "000-000" 458 | "name": "Dummy Product", 459 | "description": "Dummy Product", 460 | "short_description": "Dummy Product", 461 | "url": "", 462 | "images": { 463 | "normal": "", 464 | "small": "", 465 | "thumbnail": "", 466 | }, 467 | "is_in_stock": true, 468 | "price": { 469 | "formatted": "$0.00", 470 | "amount": 0, 471 | "currency": "USD" 472 | }, 473 | "final_price": { 474 | "formatted": "$0.00", 475 | "amount": 0, 476 | "currency": "USD" 477 | }, 478 | "is_saleable": true, 479 | "qty": 10000, 480 | "min_sale_qty": null, 481 | "max_sale_qty": 100 482 | } 483 | } 484 | 485 | Supported query parameters 486 | 487 | * attrs 488 | * comma separated list of resource attributes you want returned 489 | * sku 490 | * name 491 | * description 492 | * short_description 493 | * url 494 | * images 495 | * is_in_stock 496 | * price 497 | * final_price 498 | * is_saleable 499 | * qty 500 | * min_sale_qty 501 | * max_sale_qty 502 | 503 | 504 | ### GET /api/rest/cart/shipping_methods 505 | Return a collection of available shipping methods. 506 | **NOTE**: This collection changes as the cart data changes. 507 | 508 | [ 509 | { 510 | "code": "flaterate_flaterate" 511 | "carrier": "flaterate" 512 | "carrier_title": "Flat Rate" 513 | "method": "flaterate" 514 | "method_title": "Flat Rate" 515 | "description": "Flat rate shipping" 516 | "price": { 517 | "formatted": "$0.00" 518 | "currency": "USD", 519 | "amount": 0, 520 | } 521 | } 522 | ] 523 | 524 | Supported query parameters 525 | 526 | * attrs 527 | * comma separated list of resource attributes you want returned 528 | * code 529 | * carrier 530 | * carrier_title 531 | * method 532 | * method_title 533 | * description 534 | * price 535 | 536 | ### GET /api/rest/cart/payment_methods 537 | Return a collection of available payment methods. 538 | **NOTE**: This collection changes as the cart data changes and depends on the store configuration. 539 | 540 | [ 541 | { 542 | "code": "braintree", 543 | "title": "Credit Card ", 544 | "cc_types": { 545 | "AE": "American Express", 546 | "VI": "Visa", 547 | "MC": "MasterCard" 548 | } 549 | }, 550 | { 551 | "code": "checkmo", 552 | "title": "Check / Money order", 553 | "cc_types": null 554 | } 555 | ] 556 | 557 | ## NOTES 558 | * This module is currently being written for PHP 5.4+ and Magento CE 1.8+ support only. 559 | * When PHP 5.4 hits EOL, the minimum requirements will be updated to reflect this. 560 | * If/when Magento CE 1.10 is released then support for Magento CE 1.8 will be dropped. 561 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/Model/Item.php: -------------------------------------------------------------------------------- 1 | 'backorders', 12 | 'error_info' => 'error_infos', 13 | ]; 14 | 15 | /** 16 | * Hash of external attribute codes and their data type 17 | * 18 | * @var string[] 19 | */ 20 | protected $attributeTypeMap = [ 21 | 'item_id' => 'int', 22 | 'qty' => 'float', 23 | 'original_price' => 'currency', 24 | 'price' => 'currency', 25 | 'row_total' => 'currency', 26 | 'backorder_qty' => 'float', 27 | ]; 28 | 29 | /** 30 | * Array of external attribute codes that are manually generated 31 | * 32 | * @var string[] 33 | */ 34 | protected $manualAttributes = [ 35 | 'original_price', 36 | 'url', 37 | 'images', 38 | 'children', 39 | 'messages', 40 | 'is_saleable', 41 | ]; 42 | 43 | /** 44 | * Dispatch API call 45 | */ 46 | public function dispatch() 47 | { 48 | $quote = $this->loadQuote(); 49 | 50 | switch ($this->getActionType() . $this->getOperation()) { 51 | case self::ACTION_TYPE_COLLECTION . self::OPERATION_RETRIEVE: 52 | $this->_render($this->prepareCollection($quote)); 53 | break; 54 | case self::ACTION_TYPE_COLLECTION . self::OPERATION_CREATE: 55 | $multipleItems = $this->getMultipleItems($this->getRequest()->getBodyParams()); 56 | if ($multipleItems) { 57 | $data = []; 58 | $new = false; 59 | if (!$quote->getId()) { 60 | $this->saveQuote(); 61 | $new = true; 62 | } 63 | foreach ($multipleItems as $lineItem) { 64 | /** @var Mage_Sales_Model_Quote_Item $item */ 65 | $item = $this->createResource($quote, $lineItem); 66 | $item->save(); 67 | } 68 | $this->saveQuote(); 69 | foreach ($quote->getAllVisibleItems() as $item) { 70 | $data[] = $this->prepareResource($item); 71 | } 72 | if ($new) { 73 | $this->getResponse()->setHttpResponseCode(Mage_Api2_Model_Server::HTTP_CREATED); 74 | $this->getResponse()->setHeader('Location', $this->getRequest()->getPathInfo()); 75 | } else { 76 | $this->getResponse()->setHttpResponseCode(Mage_Api2_Model_Server::HTTP_OK); 77 | $this->getResponse()->setHeader('Content-Location', $this->getRequest()->getPathInfo()); 78 | } 79 | $this->_render($data); 80 | } else { 81 | $item = $this->createResource($quote, $this->getRequest()->getBodyParams()); 82 | $new = $item->isObjectNew(); 83 | if (!$quote->getId()) { 84 | $this->saveQuote(); 85 | } 86 | $item->save(); 87 | $this->saveQuote(); 88 | if ($new) { 89 | $this->getResponse()->setHttpResponseCode(Mage_Api2_Model_Server::HTTP_CREATED); 90 | $this->getResponse()->setHeader('Location', $this->_getLocation($item)); 91 | } else { 92 | $this->getResponse()->setHttpResponseCode(Mage_Api2_Model_Server::HTTP_OK); 93 | $this->getResponse()->setHeader('Content-Location', $this->_getLocation($item)); 94 | } 95 | $this->_render($this->prepareResource($item)); 96 | } 97 | break; 98 | case self::ACTION_TYPE_COLLECTION . self::OPERATION_DELETE: 99 | foreach ($quote->getAllVisibleItems() as $item) { 100 | /** @var Mage_Sales_Model_Quote_Item $item */ 101 | $quote->deleteItem($item); 102 | $item->delete(); 103 | } 104 | $this->saveQuote(); 105 | $this->getResponse()->setMimeType($this->getRenderer()->getMimeType()); 106 | $this->getResponse()->setHttpResponseCode(204); 107 | break; 108 | case self::ACTION_TYPE_ENTITY . self::OPERATION_RETRIEVE: 109 | $item = $this->loadItem($quote, $this->getRequest()->getParam('id')); 110 | $this->_render($this->prepareResource($item)); 111 | break; 112 | case self::ACTION_TYPE_ENTITY . self::OPERATION_CREATE: 113 | $item = $this->loadItem($quote, $this->getRequest()->getParam('id')); 114 | $this->updateResource($item, $this->getRequest()->getBodyParams()); 115 | $this->saveQuote(); 116 | $this->_render($this->prepareResource($item)); 117 | break; 118 | case self::ACTION_TYPE_ENTITY . self::OPERATION_UPDATE: 119 | $item = $this->loadItem($quote, $this->getRequest()->getParam('id')); 120 | $this->updateResource($item, $this->getRequest()->getBodyParams()); 121 | $this->saveQuote(); 122 | $this->_render($this->prepareResource($item)); 123 | break; 124 | case self::ACTION_TYPE_ENTITY . self::OPERATION_DELETE: 125 | $item = $this->loadItem($quote, $this->getRequest()->getParam('id')); 126 | $quote->deleteItem($item); 127 | $item->delete(); 128 | $this->saveQuote(); 129 | $this->getResponse()->setMimeType($this->getRenderer()->getMimeType()); 130 | $this->getResponse()->setHttpResponseCode(204); 131 | break; 132 | default: 133 | $this->_critical(self::RESOURCE_METHOD_NOT_ALLOWED); 134 | } 135 | } 136 | 137 | /** 138 | * Checks for the presence of "items" body param and being an array 139 | * 140 | * @param $bodyParams 141 | * @return false|array 142 | */ 143 | public function getMultipleItems($bodyParams) 144 | { 145 | if (isset($bodyParams['items']) && is_array($bodyParams['items'])) { 146 | return $bodyParams['items']; 147 | } 148 | 149 | return false; 150 | } 151 | 152 | /** 153 | * Convert the resource model collection to an array 154 | * 155 | * @param Mage_Sales_Model_Quote $quote 156 | * 157 | * @return array 158 | */ 159 | public function prepareCollection(Mage_Sales_Model_Quote $quote) 160 | { 161 | // Store current state 162 | $actionType = $this->getActionType(); 163 | $operation = $this->getOperation(); 164 | 165 | // Change state 166 | $this->setActionType(self::ACTION_TYPE_COLLECTION); 167 | $this->setOperation(self::OPERATION_RETRIEVE); 168 | 169 | $data = []; 170 | 171 | $filter = $this->getFilter(); 172 | foreach ($quote->getAllVisibleItems() as $item) { 173 | /** @var Mage_Sales_Model_Quote_Item $item */ 174 | // Add data to result 175 | $data[$item->getId()] = $this->prepareItem($item, $filter); 176 | } 177 | 178 | // Restore old state 179 | $this->setActionType($actionType); 180 | $this->setOperation($operation); 181 | 182 | // This collection should always be a key/value hash and never a simple array 183 | $data = new ArrayObject($data); 184 | 185 | // Return prepared outbound data 186 | return $data; 187 | } 188 | 189 | /** 190 | * Convert the resource model to an array 191 | * 192 | * @param Mage_Sales_Model_Quote_Item $resource 193 | * 194 | * @return array 195 | */ 196 | public function prepareResource(Mage_Sales_Model_Quote_Item $resource) 197 | { 198 | // Store current state 199 | $actionType = $this->getActionType(); 200 | $operation = $this->getOperation(); 201 | 202 | // Change state 203 | $this->setActionType(self::ACTION_TYPE_ENTITY); 204 | $this->setOperation(self::OPERATION_RETRIEVE); 205 | 206 | // Get raw outbound data 207 | $data = $this->prepareItem($resource, $this->getFilter()); 208 | 209 | // Restore old state 210 | $this->setActionType($actionType); 211 | $this->setOperation($operation); 212 | 213 | // Return prepared outbound data 214 | return $data; 215 | } 216 | 217 | protected function prepareItem(Mage_Sales_Model_Quote_Item $item, Mage_Api2_Model_Acl_Filter $filter) 218 | { 219 | // Get raw outbound data 220 | $data = $this->loadResourceAttributes($item, $filter->getAttributesToInclude()); 221 | 222 | // ========================= 223 | // BEGIN - Manual attributes 224 | // ========================= 225 | 226 | /** @var Mage_Tax_Model_Config $taxConfig */ 227 | $taxConfig = Mage::getModel('tax/config'); 228 | 229 | // row_total - including tax 230 | if (in_array('row_total', $filter->getAttributesToInclude()) && $taxConfig->displayCartPricesInclTax($item->getStore())) { 231 | $data['row_total'] = $item->getRowTotalInclTax(); 232 | } 233 | 234 | // original_price 235 | if (in_array('original_price', $filter->getAttributesToInclude())) { 236 | $product = $item->getProduct(); 237 | $data['original_price'] = $product->getPriceModel()->getPrice($product); 238 | } 239 | 240 | // Product URL 241 | if (in_array('url', $filter->getAttributesToInclude())) { 242 | $data['url'] = $this->getProductUrl($item); 243 | } 244 | 245 | // image URLs 246 | if (in_array('images', $filter->getAttributesToInclude())) { 247 | $data['images'] = $this->getImageUrls($item->getProduct()); 248 | } 249 | 250 | // child items 251 | if (!$item->getParentItemId() && in_array('children', $filter->getAttributesToInclude())) { 252 | $data['children'] = []; 253 | foreach ($item->getQuote()->getItemsCollection() as $quoteItem) { 254 | /** @var Mage_Sales_Model_Quote_Item $quoteItem */ 255 | if (!$quoteItem->isDeleted() && $quoteItem->getParentItemId() == $item->getId()) { 256 | $quoteItemData = $this->prepareItem($quoteItem, $filter); 257 | // Remove the children entry from a child as that kind of nesting is not allowed anyway 258 | unset($quoteItemData['children']); 259 | $data['children'][] = $quoteItemData; 260 | } 261 | } 262 | } 263 | 264 | // messages 265 | if (in_array('messages', $filter->getAttributesToInclude())) { 266 | $data['messages'] = $item->getMessage(false); 267 | } 268 | 269 | // is_saleable flag 270 | if (in_array('is_saleable', $filter->getAttributesToInclude())) { 271 | $data['is_saleable'] = (bool)$item->getProduct()->getIsSalable(); 272 | } 273 | 274 | // ========================= 275 | // END - Manual attributes 276 | // ========================= 277 | 278 | // Fire event 279 | $data = new Varien_Object($data); 280 | Mage::dispatchEvent('aoe_cartapi_item_prepare', ['data' => $data, 'filter' => $filter, 'resource' => $item]); 281 | $data = $data->getData(); 282 | 283 | // Filter outbound data 284 | $data = $filter->out($data); 285 | 286 | // Fix data types 287 | $data = $this->fixTypes($data); 288 | 289 | // Add null values for missing data 290 | foreach ($filter->getAttributesToInclude() as $code) { 291 | if (!array_key_exists($code, $data)) { 292 | $data[$code] = null; 293 | } 294 | } 295 | 296 | // Sort the result by key 297 | ksort($data); 298 | 299 | // Fire event 300 | $data = new Varien_Object($data); 301 | Mage::dispatchEvent('aoe_cartapi_item_prepare_after', ['data' => $data, 'filter' => $filter, 'resource' => $item]); 302 | $data = $data->getData(); 303 | 304 | return $data; 305 | } 306 | 307 | /** 308 | * Create a resource model 309 | * 310 | * @param Mage_Sales_Model_Quote $quote 311 | * @param array $data 312 | * 313 | * @return Mage_Sales_Model_Quote_Item 314 | */ 315 | public function createResource(Mage_Sales_Model_Quote $quote, array $data) 316 | { 317 | // Store current state 318 | $actionType = $this->getActionType(); 319 | $operation = $this->getOperation(); 320 | 321 | // Change state 322 | $this->setActionType(self::ACTION_TYPE_ENTITY); 323 | $this->setOperation(self::OPERATION_CREATE); 324 | 325 | // Filter raw incoming data 326 | $data = $this->getFilter()->in($data); 327 | 328 | // Map data keys 329 | $data = $this->mapAttributes($data); 330 | 331 | // Validate we have a SKU 332 | if (!isset($data['sku'])) { 333 | $this->_critical('Missing SKU', Mage_Api2_Model_Server::HTTP_BAD_REQUEST); 334 | } 335 | 336 | // Load product based on SKU 337 | 338 | /** @var Mage_Catalog_Model_Product $product */ 339 | $product = Mage::getModel('catalog/product') 340 | ->setStoreId(Mage::app()->getStore()->getId()) 341 | ->load(Mage::getResourceModel('catalog/product')->getIdBySku($data['sku'])); 342 | 343 | // If there is no product with that SKU, throw an error 344 | if (!$product->getId()) { 345 | $this->_critical('Invalid SKU ' . $data['sku'], Mage_Api2_Model_Server::HTTP_BAD_REQUEST); 346 | } 347 | 348 | // If the SKU is not enabled, throw an error ("isInStock" is a badly named method) 349 | if (!$product->isInStock()) { 350 | $this->_critical('Invalid SKU ' . $data['sku'], Mage_Api2_Model_Server::HTTP_BAD_REQUEST); 351 | } 352 | 353 | // If the SKU is not visible for the current website, throw an error 354 | if (!$product->isVisibleInSiteVisibility()) { 355 | $this->_critical('Invalid SKU ' . $data['sku'], Mage_Api2_Model_Server::HTTP_BAD_REQUEST); 356 | } 357 | 358 | if (!Mage::app()->isSingleStoreMode()) { 359 | // If the SKU is not available for the current website, throw an error 360 | if (!is_array($product->getWebsiteIds()) || !in_array(Mage::app()->getStore()->getWebsiteId(), $product->getWebsiteIds())) { 361 | $this->_critical('Invalid SKU ' . $data['sku'], Mage_Api2_Model_Server::HTTP_BAD_REQUEST); 362 | } 363 | } 364 | 365 | // Ensure we have a quantity 366 | if (!isset($data['qty'])) { 367 | $data['qty'] = 1; 368 | } 369 | $data['qty'] = floatval($data['qty']); 370 | 371 | // Ensure we have a min quantity if required 372 | if (!$quote->hasProductId($product->getId()) && $product->getStockItem()) { 373 | $minimumQty = floatval($product->getStockItem()->getMinSaleQty()); 374 | if ($minimumQty > 0.0 && $data['qty'] < $minimumQty) { 375 | $data['qty'] = $minimumQty; 376 | } 377 | } 378 | 379 | // Add product to quote 380 | try { 381 | $product->setSkipCheckRequiredOption(true); 382 | $resource = $quote->addProduct($product, new Varien_Object($data)); 383 | 384 | // This is to work around a bug in Mage_Sales_Model_Quote::addProductAdvanced 385 | // The method incorrectly returns $item when it SHOULD return $parentItem 386 | if ($resource instanceof Mage_Sales_Model_Quote_Item && $resource->getParentItem()) { 387 | $resource = $resource->getParentItem(); 388 | } 389 | } catch (Exception $e) { 390 | $resource = $e->getMessage(); 391 | } 392 | 393 | // Check for errors 394 | if (is_string($resource)) { 395 | $this->_critical($resource, Mage_Api2_Model_Server::HTTP_BAD_REQUEST); 396 | } 397 | 398 | // Restore old state 399 | $this->setActionType($actionType); 400 | $this->setOperation($operation); 401 | 402 | // Return updated resource 403 | return $resource; 404 | } 405 | 406 | /** 407 | * Update the resource model 408 | * 409 | * @param Mage_Sales_Model_Quote_Item $resource 410 | * @param array $data 411 | * 412 | * @return Mage_Sales_Model_Quote_Item 413 | */ 414 | public function updateResource(Mage_Sales_Model_Quote_Item $resource, array $data) 415 | { 416 | // Store current state 417 | $actionType = $this->getActionType(); 418 | $operation = $this->getOperation(); 419 | 420 | // Change state 421 | $this->setActionType(self::ACTION_TYPE_ENTITY); 422 | $this->setOperation(self::OPERATION_UPDATE); 423 | 424 | // Get a filter instance 425 | $filter = $this->getFilter(); 426 | 427 | // Fire event - before filter 428 | $data = new Varien_Object($data); 429 | Mage::dispatchEvent('aoe_cartapi_item_update_prefilter', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 430 | $data = $data->getData(); 431 | 432 | // Get allowed attributes 433 | $allowedAttributes = $filter->getAllowedAttributes(Mage_Api2_Model_Resource::OPERATION_ATTRIBUTE_WRITE); 434 | 435 | // Manually prevent SKU for updates 436 | $allowedAttributes = array_diff($allowedAttributes, ['sku']); 437 | 438 | // Update model 439 | $this->saveResourceAttributes($resource, $allowedAttributes, $data); 440 | 441 | // Fire event 442 | $data = new Varien_Object($data); 443 | Mage::dispatchEvent('aoe_cartapi_item_update', ['data' => $data, 'filter' => $filter, 'resource' => $resource]); 444 | //$data = $data->getData(); 445 | 446 | // Restore old state 447 | $this->setActionType($actionType); 448 | $this->setOperation($operation); 449 | 450 | // Return updated model 451 | return $resource; 452 | } 453 | 454 | /** 455 | * @param Mage_Sales_Model_Quote $quote 456 | * @param int $id 457 | * 458 | * @return Mage_Sales_Model_Quote_Item 459 | * 460 | * @throws Exception 461 | */ 462 | protected function loadItem(Mage_Sales_Model_Quote $quote, $id) 463 | { 464 | $id = intval($id); 465 | if (!$id) { 466 | $this->_critical('Not Found', Mage_Api2_Model_Server::HTTP_NOT_FOUND); 467 | } 468 | 469 | /** @var Mage_Sales_Model_Quote_Item $item */ 470 | $item = $quote->getItemById($id); 471 | if (!$item || $item->isDeleted() || $item->getParentItemId()) { 472 | $this->_critical('Not Found', Mage_Api2_Model_Server::HTTP_NOT_FOUND); 473 | } 474 | 475 | return $item; 476 | } 477 | 478 | protected function getProductUrl(Mage_Sales_Model_Quote_Item $item) 479 | { 480 | if ($item->getRedirectUrl()) { 481 | return $item->getRedirectUrl(); 482 | } 483 | 484 | $product = $item->getProduct(); 485 | $option = $item->getOptionByCode('product_type'); 486 | if ($option) { 487 | $product = $option->getProduct(); 488 | } 489 | 490 | return $product->getUrlModel()->getUrl($product); 491 | } 492 | 493 | protected function getImageUrls(Mage_Catalog_Model_Product $product) 494 | { 495 | $data = []; 496 | 497 | /** @var Mage_Catalog_Helper_Image $helper */ 498 | $helper = Mage::helper('catalog/image'); 499 | 500 | // Add normal URL 501 | $helper->init($product, 'image'); 502 | $size = Mage::getStoreConfig(Mage_Catalog_Helper_Image::XML_NODE_PRODUCT_BASE_IMAGE_WIDTH); 503 | if (is_numeric($size)) { 504 | $helper->constrainOnly(true)->resize($size); 505 | } 506 | $data['normal'] = $helper->__toString(); 507 | 508 | // Add small URL 509 | $helper->init($product, 'small_image'); 510 | $size = Mage::getStoreConfig(Mage_Catalog_Helper_Image::XML_NODE_PRODUCT_SMALL_IMAGE_WIDTH); 511 | if (is_numeric($size)) { 512 | $helper->constrainOnly(true)->resize($size); 513 | } 514 | $data['small'] = $helper->__toString(); 515 | 516 | // Add thumbnail URL 517 | $helper->init($product, 'thumbnail'); 518 | $size = Mage::getStoreConfig('catalog/product_image/thumbnail_width'); 519 | if (is_numeric($size)) { 520 | $helper->constrainOnly(true)->resize($size); 521 | } 522 | $data['thumbnail'] = $helper->__toString(); 523 | 524 | return $data; 525 | } 526 | } 527 | -------------------------------------------------------------------------------- /app/code/community/Aoe/CartApi/etc/api2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AOE CartApi 7 | 100 8 | 9 | 10 | 11 | 12 | 1 13 | aoe_cartapi 14 | Aoe_CartApi/Cart 15 | Cart 16 | 10 17 | 18 | 19 | 1 20 | 1 21 | 1 22 | 1 23 | 24 | 25 | 1 26 | 1 27 | 1 28 | 1 29 | 30 | 31 | 32 | Email Address 33 | Coupon Code 34 | Shipping Method 35 | Summary Quantity 36 | Totals 37 | Messages 38 | Has Error 39 | 40 | 41 | 42 | 43 | 1 44 | 1 45 | 1 46 | 1 47 | 48 | 49 | 50 | 51 | 1 52 | 1 53 | 1 54 | 1 55 | 56 | 57 | 58 | 59 | 60 | /cart 61 | entity 62 | 63 | 64 | 65 | 66 | 1 67 | aoe_cartapi 68 | Aoe_CartApi/Item 69 | Item 70 | 20 71 | 72 | 73 | 1 74 | 1 75 | 1 76 | 1 77 | 78 | 79 | 1 80 | 1 81 | 1 82 | 1 83 | 84 | 85 | 86 | Item ID (internal) 87 | SKU 88 | Name 89 | URL 90 | Images 91 | Child Items 92 | Quantity 93 | Backorder Quantity 94 | Original Price 95 | Price 96 | Row Total 97 | Messages 98 | Error Info 99 | Is Saleable 100 | 101 | 102 | 103 | 104 | 1 105 | 1 106 | 1 107 | 1 108 | 1 109 | 1 110 | 1 111 | 1 112 | 1 113 | 1 114 | 1 115 | 116 | 117 | 118 | 119 | 1 120 | 1 121 | 1 122 | 1 123 | 1 124 | 1 125 | 1 126 | 1 127 | 1 128 | 1 129 | 1 130 | 131 | 132 | 133 | 134 | 135 | /cart/items 136 | collection 137 | 138 | 139 | /cart/items/:id 140 | entity 141 | 142 | 143 | 144 | 145 | 1 146 | aoe_cartapi 147 | Aoe_CartApi/Crosssell 148 | Cross-sells 149 | 30 150 | 151 | 152 | 1 153 | 154 | 155 | 1 156 | 157 | 158 | 159 | SKU 160 | Product Name 161 | Product Description 162 | Product Short Description 163 | Product URL 164 | Images 165 | Stock Status 166 | Price 167 | Minimal Price 168 | Final Price 169 | Is Saleable 170 | Quantity 171 | Min Sale Quantity 172 | Max Sale Quantity 173 | 174 | 175 | 176 | /cart/crosssells 177 | collection 178 | 179 | 180 | 181 | 182 | 1 183 | aoe_cartapi 184 | Aoe_CartApi/BillingAddress 185 | Billing Address 186 | 40 187 | 188 | 189 | 1 190 | 1 191 | 1 192 | 1 193 | 194 | 195 | 1 196 | 1 197 | 1 198 | 1 199 | 200 | 201 | 202 | customer_address_id 203 | save_in_address_book 204 | prefix 205 | firstname 206 | middlename 207 | lastname 208 | suffix 209 | company 210 | street 211 | city 212 | region 213 | postcode 214 | country_id 215 | telephone 216 | fax 217 | save_in_address_book 218 | HTML Formatted Address 219 | Text Formatted Address 220 | Validation Errors 221 | 222 | 223 | 224 | 225 | 1 226 | 1 227 | 228 | 229 | 1 230 | 1 231 | 1 232 | 1 233 | 1 234 | 235 | 236 | 237 | 238 | 1 239 | 1 240 | 1 241 | 242 | 243 | 244 | 245 | 246 | /cart/billing_address 247 | entity 248 | 249 | 250 | 251 | 252 | 1 253 | aoe_cartapi 254 | Aoe_CartApi/ShippingAddress 255 | Shipping Address 256 | 50 257 | 258 | 259 | 1 260 | 1 261 | 1 262 | 1 263 | 264 | 265 | 1 266 | 1 267 | 1 268 | 1 269 | 270 | 271 | 272 | customer_address_id 273 | save_in_address_book 274 | same_as_billing 275 | prefix 276 | firstname 277 | middlename 278 | lastname 279 | suffix 280 | company 281 | street 282 | city 283 | region 284 | postcode 285 | country_id 286 | telephone 287 | fax 288 | save_in_address_book 289 | HTML Formatted Address 290 | Text Formatted Address 291 | Validation Errors 292 | 293 | 294 | 295 | 296 | 1 297 | 1 298 | 299 | 300 | 1 301 | 1 302 | 1 303 | 1 304 | 1 305 | 306 | 307 | 308 | 309 | 1 310 | 1 311 | 1 312 | 313 | 314 | 315 | 316 | 317 | /cart/shipping_address 318 | entity 319 | 320 | 321 | 322 | 323 | 1 324 | aoe_cartapi 325 | Aoe_CartApi/Payment 326 | Payment 327 | 60 328 | 329 | 330 | 1 331 | 1 332 | 1 333 | 1 334 | 335 | 336 | 1 337 | 1 338 | 1 339 | 1 340 | 341 | 342 | 343 | Payment Method 344 | Payment Data 345 | 346 | 347 | 348 | /cart/payment 349 | entity 350 | 351 | 352 | 353 | 354 | 355 | 1 356 | aoe_cartapi 357 | Aoe_CartApi/PaymentMethods 358 | Payment Methods 359 | 70 360 | 361 | 362 | 1 363 | 364 | 365 | 1 366 | 367 | 368 | 369 | Payment Method Code 370 | Payment Method Title 371 | Payment CC Type 372 | Payment Form Block Type 373 | 374 | 375 | 376 | /cart/payment_methods 377 | collection 378 | 379 | 380 | 381 | 382 | 383 | 1 384 | aoe_cartapi 385 | Aoe_CartApi/Validate 386 | Validate 387 | 110 388 | 389 | 390 | 1 391 | 392 | 393 | 1 394 | 395 | 396 | 397 | Errors 398 | 399 | 400 | 401 | 1 402 | 403 | 404 | 1 405 | 406 | 407 | 408 | 409 | /cart/validate 410 | entity 411 | 412 | 413 | 414 | 415 | 1 416 | aoe_cartapi 417 | Aoe_CartApi/Place 418 | Place 419 | 120 420 | 421 | 422 | 1 423 | 424 | 425 | 1 426 | 427 | 428 | 429 | Order Increment ID 430 | Order URL 431 | Errors 432 | 433 | 434 | 435 | 1 436 | 437 | 438 | 1 439 | 440 | 441 | 442 | 443 | /cart/place 444 | entity 445 | 446 | 447 | 448 | 449 | 450 | 1 451 | aoe_cartapi 452 | Aoe_CartApi/ShippingMethod 453 | Shipping Methods 454 | 210 455 | 456 | 457 | 1 458 | 459 | 460 | 1 461 | 462 | 463 | 464 | Code 465 | Carrier Code 466 | Carrier Title 467 | Method Code 468 | Method Title 469 | Description 470 | Price 471 | 472 | 473 | 474 | /cart/shipping_methods 475 | collection 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | --------------------------------------------------------------------------------