diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f19e98c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +composer.lock +.DS_Store diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1a8c85f --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +# override like so: make dependencies COMPOSER=$(which composer.phar) +COMPOSER = ./build/composer.phar + +.PHONY : test +test: + php ./test/Affirm.php + +.PHONY : dependencies +dependencies: + $(COMPOSER) update --dev diff --git a/README.md b/README.md new file mode 100644 index 0000000..fed69cf --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +[![](docs/splash.png)](https://affirm.com) + +**Compatible with** + +Magento CE 1.4.0.1+ + +Install +------- + +**To install using [modgit](https://github.com/jreinke/modgit):** + +``` +cd MAGENTO_ROOT +modgit -i extension/:. add Magento_Affirm git@github.com:Affirm/Magento_Affirm.git +``` +to update: +``` +modgit update Magento_Affirm +``` + +**To install using [modman](https://github.com/colinmollenhour/modman):** + +``` +cd MAGENTO_ROOT +modman clone git@github.com:Affirm/Magento_Affirm.git +``` +to update: +``` +modman update Magento_Affirm +``` + +**To install using Affirm's deploy script:** + +1. Download the [Makefile](https://gist.githubusercontent.com/perfmode/ad8cf189bbbaeeae1181/raw/6fb5e861a6dddc8cd6685573e492b0662c498a15/Makefile) (requires wget) +2. Copy to MAGENTO_ROOT +3. To install, run `make install` +4. To update, run `make update` + +Configure +--------- + +1. Log in to your Magento Admin portal. +2. Visit System > Configuration > Payment Methods (under Sales) > Affirm +3. Set the API URL. In a test environment, use ```https://sandbox.affirm.com```. On your live site, use ```https://www.affirm.com```. +4. Provide your 3 keys (merchant API key, secret key, financial product key) +5. Adjust the order total minimum and maximum options to control when Affirm is + shown to your customers. + +![](docs/config.png) + + +Contribute +---------- + +1. Fork the repo +2. Create your feature branch (```git checkout -b my-new-feature```). +3. Commit your changes (```git commit -am 'Added some feature'```) +4. Push to the branch (```git push origin my-new-feature```) +5. Create a Pull Request + +**To run the tests:** + +``` +make dependencies +make test +``` diff --git a/build/composer.phar b/build/composer.phar new file mode 100755 index 0000000..3e369fa Binary files /dev/null and b/build/composer.phar differ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7a04888 --- /dev/null +++ b/composer.json @@ -0,0 +1,29 @@ +{ + "name": "affirm/affirm-php", + "description": "Affirm PHP bindings", + "keywords": [ + "affirm", + "api", + "consumer finance", + "credit", + "e-commerce", + "payment method" + ], + "homepage": "https://affirm.com/", + "authors": [ + { + "name": "The Affirm Technology Team", + "homepage": "https://www.affirm.com/company" + } + ], + "require": { + "php": ">=5.2" + }, + "require-dev": { + "zendframework/zendframework1": "1.12.6", + "simpletest/simpletest": "1.1.*" + }, + "autoload": { + "classmap": ["extension/lib/Affirm/"] + } +} diff --git a/docs/config.png b/docs/config.png new file mode 100644 index 0000000..adf828d Binary files /dev/null and b/docs/config.png differ diff --git a/docs/splash.png b/docs/splash.png new file mode 100644 index 0000000..612b4db Binary files /dev/null and b/docs/splash.png differ diff --git a/extension/app/code/community/Affirm/Affirm/Block/Payment/Form.php b/extension/app/code/community/Affirm/Affirm/Block/Payment/Form.php new file mode 100644 index 0000000..9bea8c1 --- /dev/null +++ b/extension/app/code/community/Affirm/Affirm/Block/Payment/Form.php @@ -0,0 +1,60 @@ +replaceLabel(); + } + + protected function _toHtml() + { + // TODO(brian): extract this html block to a template + // TODO(brian): extract css + $msg = "You'll complete your payment after you place your order."; + + $html = ""; + + return $html; + } + + /* Replaces default label with custom image, conditionally displaying text + * based on the Affirm product. + * + * Context: Payment Information step of Checkout flow + */ + private function replaceLabel() + { + $this->setMethodTitle(""); // removes default title + + // TODO(brian): extract html to template + // TODO(brian): conditionally load based on env config option + // This is a stopgap until the promo API is ready to go + $logoSrc = "https://cdn1.affirm.com/images/badges/affirm-card_78x54.png"; + $html = " "; + + // TODO(brian): conditionally display based on payment type + // alt message: $html.= "Buy Now and Pay Later"; + $html.= "3 Monthly Payments with Split Pay"; + + $this->setMethodLabelAfterHtml($html); + } +} diff --git a/extension/app/code/community/Affirm/Affirm/Block/Payment/Info.php b/extension/app/code/community/Affirm/Affirm/Block/Payment/Info.php new file mode 100644 index 0000000..54c500e --- /dev/null +++ b/extension/app/code/community/Affirm/Affirm/Block/Payment/Info.php @@ -0,0 +1,11 @@ +Affirm Split Pay'; + return $html; + } + +} diff --git a/extension/app/code/community/Affirm/Affirm/Block/Payment/Redirect.php b/extension/app/code/community/Affirm/Affirm/Block/Payment/Redirect.php new file mode 100644 index 0000000..78425b9 --- /dev/null +++ b/extension/app/code/community/Affirm/Affirm/Block/Payment/Redirect.php @@ -0,0 +1,19 @@ +getOrder(); + $payment_method = $order->getPayment()->getMethodInstance(); + + $html = ''; + $html.= ''; + $html.= ''; + $html.= ''; + return $html; + } +} diff --git a/extension/app/code/community/Affirm/Affirm/Helper/Data.php b/extension/app/code/community/Affirm/Affirm/Helper/Data.php new file mode 100644 index 0000000..aa50654 --- /dev/null +++ b/extension/app/code/community/Affirm/Affirm/Helper/Data.php @@ -0,0 +1,6 @@ +getAcceptedCurrencyCodes())) { + return false; + } + return true; + } + + /** + * Return array of currency codes supplied by Payment Gateway + * + * @return array + */ + public function getAcceptedCurrencyCodes() + { + if (!$this->hasData('_accepted_currency')) { + $acceptedCurrencyCodes = $this->_allowCurrencyCode; + $acceptedCurrencyCodes[] = $this->getConfigData('currency'); + $this->setData('_accepted_currency', $acceptedCurrencyCodes); + } + return $this->_getData('_accepted_currency'); + } + + public function getChargeId() + { + return $this->getInfoInstance()->getAdditionalInformation("charge_id"); + } + + protected function setChargeId($charge_id) + { + return $this->getInfoInstance()->setAdditionalInformation("charge_id", $charge_id); + } + + public function getBaseApiUrl() + { + return $this->getConfigData('api_url'); + } + + // TODO(brian): extract to a separate class and use DI to make it testable/mockable + public function _api_request($method, $path, $data=null) + { + $url = trim($this->getBaseApiUrl(), "/") . self::API_CHARGES_PATH . $path; + + $client = new Zend_Http_Client($url); + + if ($method == Zend_Http_Client::POST && $data) + { + $json = json_encode($data); + $client->setRawData($json, 'application/json'); + } + + $client->setAuth($this->getConfigData('api_key'), $this->getConfigData('secret_key'), Zend_Http_Client::AUTH_BASIC); + + $raw_result = $client->request($method)->getRawBody(); + try{ + $ret_json = Zend_Json::decode($raw_result, Zend_Json::TYPE_ARRAY); + } catch(Zend_Json_Exception $e) + { + Mage::throwException(Mage::helper('affirm')->__('Invalid affirm response: '. $raw_result)); + } + + //validate to make sure there are no errors here + if (isset($ret_json["status_code"])) + { + Mage::throwException(Mage::helper('affirm')->__('Affirm error code:'. $ret_json["status_code"] . ' error: '. @$ret_json["message"])); + } + return $ret_json; + } + + protected function _set_charge_result($result) + { + if (isset($result["id"])) + { + $this->setChargeId($result["id"]); + } + else + { + Mage::throwException(Mage::helper('affirm')->__('Affirm charge id not returned from call.')); + } + } + + protected function _validate_amount_result($amount, $result) + { + if ($result["amount"] != $amount) + { + Mage::throwException(Mage::helper('affirm')->__('Affirm authorized amount of ' . $result["amount"].' does not match requested amount of: ' . $amount)); + } + } + + /** + * Send capture request to gateway + * + * @param Mage_Payment_Model_Info $payment + * @param decimal $amount + * @return Mage_Paygate_Model_Authorizenet + */ + public function capture(Varien_Object $payment, $amount) + { + if ($amount <= 0) { + Mage::throwException(Mage::helper('affirm')->__('Invalid amount for capture.')); + } + $charge_id = $this->getChargeId(); + $amount_cents = Affirm_Util::formatCents($amount); + if (!$charge_id) { + Mage::throwException(Mage::helper('affirm')->__('Charge id have not been set.')); + } + $result = $this->_api_request(Varien_Http_Client::POST, "{$charge_id}/capture"); + $this->_validate_amount_result($amount_cents, $result); + return $this; + } + + /** + * Refund capture + * + * @param Mage_Sales_Model_Order_Payment $payment + * @return Mage_Paypal_Model_Direct + */ + public function refund(Varien_Object $payment, $amount) + { + if ($amount <= 0) { + Mage::throwException(Mage::helper('affirm')->__('Invalid amount for refund.')); + } + $charge_id = $this->getChargeId(); + $amount_cents = Affirm_Util::formatCents($amount); + if (!$charge_id) { + Mage::throwException(Mage::helper('affirm')->__('Charge id have not been set.')); + } + $result = $this->_api_request(Varien_Http_Client::POST, "{$charge_id}/refund", array( + "amount"=>$amount_cents) + ); + $this->_validate_amount_result($amount_cents, $result); + + return $this; + } + + public function void(Varien_Object $payment) + { + if (!$this->canVoid($payment)) { + Mage::throwException(Mage::helper('payment')->__('Void action is not available.')); + } + $charge_id = $this->getChargeId(); + if (!$charge_id) { + Mage::throwException(Mage::helper('affirm')->__('Charge id have not been set.')); + } + $result = $this->_api_request(Varien_Http_Client::POST, "{$charge_id}/void"); + return $this; + } + + /** + * Send authorize request to gateway + * + * @param Mage_Payment_Model_Info $payment + * @param decimal $amount + * @return Mage_Paygate_Model_Authorizenet + */ + public function authorize(Varien_Object $payment, $amount) + { + if ($amount <= 0) { + Mage::throwException(Mage::helper('affirm')->__('Invalid amount for authorization.')); + } + + $amount_cents = Affirm_Util::formatCents($amount); + $token = $payment->getAdditionalInformation(self::CHECKOUT_TOKEN); + + $result = $this->_api_request(Varien_Http_Client::POST, "", array( + self::CHECKOUT_TOKEN=>$token) + ); + + $this->_set_charge_result($result); + $this->_validate_amount_result($amount_cents, $result); + $payment->setTransactionId($this->getChargeId())->setIsTransactionClosed(0); + return $this; + } + + /** + * Instantiate state and set it to state object + * @param string $paymentAction + * @param Varien_Object + */ + public function initialize($paymentAction, $stateObject) + { + $state = Mage_Sales_Model_Order::STATE_PENDING_PAYMENT; + $stateObject->setState($state); + $stateObject->setStatus('pending_payment'); + $stateObject->setIsNotified(false); + } + + + public function processConfirmOrder($order, $checkout_token) + { + $payment = $order->getPayment(); + + $payment->setAdditionalInformation(self::CHECKOUT_TOKEN, $checkout_token); + $action = $this->getConfigData('payment_action'); + + //authorize the total amount. + Affirm_Affirm_Model_Payment::authorizePaymentForOrder($payment, $order); + $payment->setAmountAuthorized(static::_affirmTotal($order)); + $order->save(); + //can capture as well.. + if ($action == self::ACTION_AUTHORIZE_CAPTURE) + { + $payment->setAmountAuthorized(static::_affirmTotal($order)); + + // TODO(brian): It is unclear why this statement is here. If you + // know why, please replace this message with documentation to + // justify its existence. + $payment->setBaseAmountAuthorized($order->getBaseTotalDue()); + + $payment->capture(null); + $order->save(); + } + } + + /** + * Return Order place redirect url + * + * @return string + */ + public function getOrderPlaceRedirectUrl() + { + return Mage::getUrl('affirm/payment/redirect', array('_secure' => true)); + } + + public function formatCents($currency, $amount) + { + return Affirm_Util::formatCents($amount); + } + + public function getCheckoutObject($order) + { + $info = $this->getInfoInstance(); // TODO(brian): remove unused variable + $shipping_address = $order->getShippingAddress(); + $shipping = null; + if ($shipping_address) + { + $shipping = array( + "name"=> array("full"=>$shipping_address->getName()), + "address"=> array( + "line1" => $shipping_address->getStreet(1), + "line2" => $shipping_address->getStreet(2), + "city" => $shipping_address->getCity(), + "state" => $shipping_address->getRegion(), + "country" => $shipping_address->getCountryModel()->getIso2Code(), + "zipcode" => $shipping_address->getPostcode(), + )); + } + + $billing_address = $order->getBillingAddress(); + $billing = array( + "email"=>$order->getCustomerEmail(), + "name"=> array("full"=>$billing_address->getName()), + "address"=> array( + "line1" => $billing_address->getStreet(1), + "line2" => $billing_address->getStreet(2), + "city" => $billing_address->getCity(), + "state" => $billing_address->getRegion(), + "country" => $billing_address->getCountryModel()->getIso2Code(), + "zipcode" => $billing_address->getPostcode(), + )); + + $items = array(); + $currency = $order->getOrderCurrency(); + $products = Mage::getModel('catalog/product'); + // TODO(brian): instantiate |pricer| upon construction + $pricer = Mage::getModel('affirm/pricer'); + foreach($order->getAllVisibleItems() as $order_item) + { + $productId = $order_item->getProductId(); + $product = $products->load($productId); + + $items[] = array( + "sku" => $order_item->getSku(), + "display_name" => $order_item->getName(), + "item_url" => $product->getProductUrl(), + "item_image_url" => $product->getImageUrl(), + "qty" => intval($order_item->getQtyOrdered()), + "unit_price" => $pricer->getPriceInCents($order_item) + ); + } + + // TODO(brian): test checkout/onepage urls. it's unclear whether this + // is enabled for all merchants or whether merchant customization could + // cause this to be an invalid destination + $checkout = array( + 'checkout_id'=>$order->getIncrementId(), + 'currency'=>$order->getOrderCurrencyCode(), + 'shipping_amount'=>$this->formatCents($currency, $order->getShippingAmount()), + 'shipping_type'=>$order->getShippingMethod(), + 'tax_amount'=>$this->formatCents($currency, $order->getTaxAmount()), + "merchant" => array( + "public_api_key"=>$this->getConfigData('api_key'), + "user_confirmation_url"=>Mage::getUrl("affirm/payment/confirm"), + "user_cancel_url"=>Mage::helper('checkout/url')->getCheckoutUrl(), + "charge_declined_url"=>Mage::helper('checkout/url')->getCheckoutUrl() + ), + "config" => array("required_billing_fields"=> "name,address,email"), + "items" => $items, + "billing" => $billing); + + // By convention, Affirm expects positive value for discount amount. + // Magento provides negative. + $discountAmtAffirm = -1 * $order->getDiscountAmount(); + if ($discountAmtAffirm > 0.001) + { + $checkout["discounts"] = array( + $order->getCouponCode()=>array( + "discount_amount"=>$this->formatCents($currency, $discountAmtAffirm) + ) + ); + } + + if ($shipping) + { + $checkout["shipping"] = $shipping; + } + $checkout['financial_product_key'] = $this->getConfigData('financial_product_key'); + + // TODO(brian): make this safer and less error-prone. + $checkout['total'] = Affirm_Util::formatCents(static::_affirmTotal($order)); + $checkout['meta'] = static::_getMetadata(); + return $checkout; + } + + // TODO(brian): extract string name constant + private static function _getMetadata() + { + return array( + "source" => array( + "data" => array( + "is_logged_in" => Mage::getSingleton('customer/session')->isLoggedIn(), + "magento_version" => Mage::getVersion() + ), + "client_name" => "magento_affirm", + "version" => Mage::getConfig()->getModuleConfig('Affirm_Affirm')->version + ) + ); + } + + /* A hacky thing used to access a private method (authorize(...)) on the + * payment object in order to provide compatibility with version 1.4.0.1 CE. + * + * FIXME(brian): take a closer look at the payment class at version 1.4. + * Surely, there _must_ be a way to accomplish this without reflection. + * + * TODO(brian): Write a regression test to catch incompatibilities with + * other Magento versions. + */ + private static function authorizePaymentForOrder($payment, $order) + { + $moduleVersion = Mage::getConfig()->getModuleConfig("Mage_Sales")->version; + $incompatibleVersions = array( + "0.9.56" + ); + if (in_array($moduleVersion, $incompatibleVersions)) { + Affirm_Affirm_Model_Payment::callPrivateMethod($payment, "_authorize", true, static::_affirmTotal($order)); + } else { + $payment->authorize(true, static::_affirmTotal($order)); + } + } + + // TODO(brian): move this function to a helper library + private static function callPrivateMethod($object, $methodName) + { + $reflectionClass = new \ReflectionClass($object); + $reflectionMethod = $reflectionClass->getMethod($methodName); + $reflectionMethod->setAccessible(true); + + $params = array_slice(func_get_args(), 2); //get all the parameters after $methodName + return $reflectionMethod->invokeArgs($object, $params); + } + + // TODO(brian): move this to an external pricer so merchants can override + // the functionality. + private static function _affirmTotal($order) + { + return $order->getTotalDue(); + } +} diff --git a/extension/app/code/community/Affirm/Affirm/Model/Pricer.php b/extension/app/code/community/Affirm/Affirm/Model/Pricer.php new file mode 100644 index 0000000..8811fc3 --- /dev/null +++ b/extension/app/code/community/Affirm/Affirm/Model/Pricer.php @@ -0,0 +1,11 @@ +getPrice()); + } +} diff --git a/extension/app/code/community/Affirm/Affirm/Model/Source/PaymentAction.php b/extension/app/code/community/Affirm/Affirm/Model/Source/PaymentAction.php new file mode 100644 index 0000000..dc3f8dd --- /dev/null +++ b/extension/app/code/community/Affirm/Affirm/Model/Source/PaymentAction.php @@ -0,0 +1,17 @@ + Affirm_Affirm_Model_Payment::ACTION_AUTHORIZE, + 'label' => Mage::helper('affirm')->__('Authorize Only') + ), + array( + 'value' => Affirm_Affirm_Model_Payment::ACTION_AUTHORIZE_CAPTURE, + 'label' => Mage::helper('affirm')->__('Authorize and Capture') + ), + ); + } +} diff --git a/extension/app/code/community/Affirm/Affirm/controllers/PaymentController.php b/extension/app/code/community/Affirm/Affirm/controllers/PaymentController.php new file mode 100644 index 0000000..02568cf --- /dev/null +++ b/extension/app/code/community/Affirm/Affirm/controllers/PaymentController.php @@ -0,0 +1,62 @@ +_quote) { + $this->_quote = $this->_getCheckoutSession()->getQuote(); + } + return $this->_quote; + } + + public function redirectAction() + { + $session = $this->_getCheckoutSession(); + if (!$session->getLastRealOrderId()) + { + $session->addError($this->__('Your order has expired.')); + $this->_redirect('checkout/cart'); + return; + } + $order = Mage::getModel('sales/order')->loadByIncrementId($session->getLastRealOrderId()); + $this->getResponse()->setBody($this->getLayout()->createBlock('affirm/payment_redirect')->setOrder($order)->toHtml()); + $session->unsQuoteId(); + $session->unsRedirectUrl(); + } + + + public function confirmAction() + { + $session = $this->_getCheckoutSession(); + $checkout_token = $this->getRequest()->getParam("checkout_token"); + if (!$checkout_token) + { + Mage::throwException($this->__('Confirm has no checkout token.')); + } + + if ($session->getLastRealOrderId()) { + $data = $this->getRequest()->getPost(); // TODO(brian): remove dead code + $order = Mage::getModel('sales/order')->loadByIncrementId($session->getLastRealOrderId()); + $order->getPayment()->getMethodInstance()->processConfirmOrder($order, $checkout_token); + + // TODO(brian): add a boolean configuration option to allow + // merchants to decide whether affirm should send emails upon email + // confirmation. + $order->sendNewOrderEmail(); + + $this->_redirect('checkout/onepage/success'); + return; + } + $this->_redirect('checkout/onepage'); + } + + // TODO(brian): implement cancel action +} diff --git a/extension/app/code/community/Affirm/Affirm/etc/config.xml b/extension/app/code/community/Affirm/Affirm/etc/config.xml new file mode 100644 index 0000000..dde0289 --- /dev/null +++ b/extension/app/code/community/Affirm/Affirm/etc/config.xml @@ -0,0 +1,55 @@ + + + + + 0.3.1 + + + + + + Affirm_Affirm_Model + + + + + Affirm_Affirm_Block + + + + + Affirm_Affirm_Helper + + + + + + + standard + + Affirm_Affirm + affirm + + + + + + + + 0 + affirm/payment + + + + authorize + https://www.affirm.com/ + + + 1 + 1 + 1 + USD + + + + diff --git a/extension/app/code/community/Affirm/Affirm/etc/system.xml b/extension/app/code/community/Affirm/Affirm/etc/system.xml new file mode 100644 index 0000000..0485057 --- /dev/null +++ b/extension/app/code/community/Affirm/Affirm/etc/system.xml @@ -0,0 +1,95 @@ + + + + + + + + text + 10 + 1 + 1 + 1 + + + + select + affirm/source_paymentAction + 2 + 1 + 1 + 0 + + + + select + adminhtml/system_config_source_yesno + 10 + 1 + 1 + 0 + + + + text + 40 + 1 + 1 + 0 + + + + text + 40 + 1 + 1 + 0 + + + + obscure + adminhtml/system_config_backend_encrypted + 50 + 1 + 1 + 0 + + + + text + 60 + 1 + 1 + 0 + + + + select + adminhtml/system_config_source_currency + 100 + 1 + 1 + 0 + + + + text + 180 + 1 + 1 + 0 + + + + text + 190 + 1 + 1 + 0 + + + + + + + diff --git a/extension/app/etc/modules/Affirm_Affirm.xml b/extension/app/etc/modules/Affirm_Affirm.xml new file mode 100644 index 0000000..a305c95 --- /dev/null +++ b/extension/app/etc/modules/Affirm_Affirm.xml @@ -0,0 +1,10 @@ + + + + + true + community + + + + diff --git a/extension/lib/Affirm/Affirm.php b/extension/lib/Affirm/Affirm.php new file mode 100644 index 0000000..71dd160 --- /dev/null +++ b/extension/lib/Affirm/Affirm.php @@ -0,0 +1,4 @@ +, and either install it ". + "in your PHP include_path or put it in the test/ directory.\n"; + exit(1); +} + +require_once(dirname(__FILE__) . '/../extension/lib/Affirm/Affirm.php'); + +require_once(dirname(__FILE__) . "/Affirm/UtilTest.php"); diff --git a/test/Affirm/UtilTest.php b/test/Affirm/UtilTest.php new file mode 100644 index 0000000..6a38b98 --- /dev/null +++ b/test/Affirm/UtilTest.php @@ -0,0 +1,89 @@ +assertSame("10.45", Affirm_Util::formatMoney(10.45)); + } + + /* If implementation uses money_format to convert a float value to a string, + * the current locale will affect the result. This is a sanity check to keep + * the implementer honest. + */ + public function testFormatMoneyNotAffectedByLocale() { + setlocale(LC_MONETARY, 'en_US'); + $this->assertSame("10010.45", Affirm_Util::formatMoney(10010.45)); + + setlocale(LC_MONETARY, 'nl_NL'); + $this->assertSame("10.45", Affirm_Util::formatMoney(10.45)); + + } + + public function testFormatMoneyZero() { + $this->assertSame("0.00", Affirm_Util::formatMoney(0)); + } + + /* + * test formatCents + * + * Testing Methodology: Hit (1) each order of magnitude up to 10^5 cents making + * sure to hit (2) negative quantities and (3) quantities with trailing 0's. + * + * 10^5 is sufficient. An incorrect implementation could introduce ',' + * comma's separating the hundreds and thousands places. + */ + + public function testFormatCentsEmpty() { + $this->assertSame(0, Affirm_Util::formatCents(null)); + } + + public function testFormatCentsBugsCaught() { + $floatAmount = 2299.99; + $naive = 229998; + $expected = 229999; + // NB: weird things happen to floats + $this->assertSame($naive, (int) (2299.99 * 100)); + $this->assertSame($expected, Affirm_Util::formatCents(2299.99)); + } + + // 10^0 + public function testFormatCentsOnes() { + $this->assertSame(0, Affirm_Util::formatCents(0)); + $this->assertSame(7, Affirm_Util::formatCents(0.07)); + $this->assertSame(-7, Affirm_Util::formatCents(-0.07)); + } + + // 10^1 + public function testFormatCentsTens() { + $this->assertSame(10, Affirm_Util::formatCents(0.10)); + $this->assertSame(12, Affirm_Util::formatCents(0.12)); + $this->assertSame(-12, Affirm_Util::formatCents(-0.12)); + } + + // 10^2 + public function testFormatCentsHundreds() { + $this->assertSame(120, Affirm_Util::formatCents(1.20)); + $this->assertSame(123, Affirm_Util::formatCents(1.23)); + $this->assertSame(-123, Affirm_Util::formatCents(-1.23)); + } + + // 10^3 + public function testFormatCentsThousands() { + $this->assertSame(1230, Affirm_Util::formatCents(12.30)); + $this->assertSame(1234, Affirm_Util::formatCents(12.34)); + $this->assertSame(-1234, Affirm_Util::formatCents(-12.34)); + } + + // 10^4 + public function testFormatCentsTensOfThousands() { + $this->assertSame(12340, Affirm_Util::formatCents(123.40)); + $this->assertSame(12345, Affirm_Util::formatCents(123.45)); + $this->assertSame(-12345, Affirm_Util::formatCents(-123.45)); + } + + // 10^5 + public function testFormatCentsHundredThousands() { + $this->assertSame(123450, Affirm_Util::formatCents(1234.50)); + $this->assertSame(123456, Affirm_Util::formatCents(1234.56)); + $this->assertSame(-123456, Affirm_Util::formatCents(-1234.56)); + } +} diff --git a/util/Makefile b/util/Makefile new file mode 100644 index 0000000..797da6f --- /dev/null +++ b/util/Makefile @@ -0,0 +1,38 @@ +# __ __ _ +# / _| / _|(_) +# __ _ | |_ | |_ _ _ __ _ __ ___ +# / _` || _|| _|| || '__|| '_ ` _ \ +# | (_| || | | | | || | | | | | | | +# \__,_||_| |_| |_||_| |_| |_| |_| +# +# Deploy script for Affirm's Payment Method Extension for the Magento eCommerce Platform +# -------------------------------------------------------------------------------------- +# +# Place this script in your Magento root and commit it to your version control system. +# + +NAME = Magento_Affirm + +REPO = git@github.com:Affirm/Magento_Affirm.git +BRANCH = master + +SRC = extension/ +DEST = . + +SCRIPT = aff-modgit +CMD = ./$(SCRIPT) + +.modgit: $(SCRIPT) + $(CMD) init + +install: $(SCRIPT) + $(CMD) -i $(SRC):$(DEST) -b $(BRANCH) add $(NAME) $(REPO) + +update: $(SCRIPT) + $(CMD) up $(NAME) + +remove: $(SCRIPT) + $(CMD) rm $(NAME) + +$(SCRIPT): + wget -O $(SCRIPT) https://raw.githubusercontent.com/Affirm/modgit/master/modgit