v7‰PNG  IHDR Ÿ f Õ†C1 sRGB ®Îé gAMA ± üa pHYs à ÃÇo¨d GIDATx^íÜL”÷ð÷Yçªö("Bh_ò«®¸¢§q5kÖ*:þ0A­ºšÖ¥]VkJ¢M»¶f¸±8\k2íll£1]q®ÙÔ‚ÆT getUser()->data()->get(self::TOKEN); if (!$token) return $result->setErrorMessages(array(___('Payment failed'))); if ($doFirst && (doubleval($invoice->first_total) <= 0)) { // free trial $tr = new Am_Paysystem_Transaction_Free($this); $tr->setInvoice($invoice); $tr->process(); $result->setSuccess($tr); } else { $tr = new Am_Paysystem_Transaction_Stripe($this, $invoice, $doFirst); $tr->run($result); } } public function getUpdateCcLink($user) { if ($user->data()->get(self::TOKEN)) return $this->getPluginUrl('update'); } public function storeCreditCard(CcRecord $cc, Am_Paysystem_Result $result) { } public function loadCreditCard(Invoice $invoice) { if ($invoice->getUser()->data()->get(self::TOKEN)) return $this->getDi()->CcRecordTable->createRecord(); // return fake record for rebill } protected function createController(Am_Mvc_Request $request, Am_Mvc_Response $response, array $invokeArgs) { $invokeArgs['hosted'] = $this->getConfig('hosted'); return new Am_Mvc_Controller_CreditCard_Stripe($request, $response, $invokeArgs); } protected function _initSetupForm(Am_Form_Setup $form) { $form->addText('secret_key', array('class'=>'el-wide'))->setLabel('Secret Key')->addRule('required'); $form->addText('public_key', array('class'=>'el-wide'))->setLabel('Publishable Key')->addRule('required'); $label = "Use Hosted Version (recommended)\n". "this option allows you to display credit card input right on your website\n". "(as a popup) and in the same time it does not require PCI DSS compliance"; if ('https' != substr(ROOT_SURL,0,5)) $label .= "\n" . 'This option requires https on your site'; $form->addAdvCheckbox('hosted', array('id'=>'hosted-version'))->setLabel($label); $form->addText('image_url', array('class'=>'el-wide', 'rel'=>'hosted-version')) ->setLabel("Image URL\nA relative or absolute URL to a square image of your brand or product. Recommended minimum size is 128x128px. Supported image types are: .gif, .jpeg, and .png."); $form->addAdvCheckbox('zip_validate', array('rel'=>'hosted-version')) ->setLabel("Validate Postal Code?\nSpecify whether Checkout should validate the billing postal code"); $form->addScript() ->setScript(<<getInvoice(), $payment->receipt_id, $amount); $tr->run($result); } public function createTransaction(Am_Mvc_Request $request, Am_Mvc_Response $response, array $invokeArgs) { return new Am_Paysystem_Transaction_Stripe_Webhook($this, $request, $response, $invokeArgs); } public function getReadme() { $url = $this->getPluginUrl('ipn'); return <<charge.refunded $url CUT; } } class Am_Mvc_Controller_CreditCard_Stripe extends Am_Mvc_Controller { /** @var Invoice*/ protected $invoice; /** @var Am_Paysystem_Stripe */ protected $plugin; public function setInvoice(Invoice $invoice) { $this->invoice = $invoice; } public function setPlugin($plugin) { $this->plugin = $plugin; } public function createForm($label, $cc_mask = null) { if($this->getInvokeArg('hosted')) return $this->createFormHosted($label, $cc_mask); else return $this->createFormRegular($label, $cc_mask); } public function createFormHosted($label, $cc_mask = null) { $form = new Am_Form('cc-stripe'); $key = $this->plugin->getConfig('public_key'); $amount = $this->invoice->first_total*100; $currency = $this->invoice->currency; $email = $this->invoice->getEmail(); $name = $this->getDi()->config->get('site_title'); $description = $this->invoice->getLineDescription(); $lang = $this->getDi()->locale->getLanguage(); $plabel = $label; $image = $this->plugin->getConfig('image_url'); $zipvalidate = $this->plugin->getConfig('zip_validate') ? 'true' : 'false'; $form->addHidden('id')->setValue($this->_request->get('id')); $form->addStatic()->setContent(<< CUT ); $form->addScript()->setScript(<<setDataSources(array( $this->_request, new HTML_QuickForm2_DataSource_Array($this->getDefaultValues($this->invoice->getUser())) )); return $form; } public function createFormRegular($label, $cc_mask = null) { $form = new Am_Form('cc-stripe'); $name = $form->addGroup() ->setLabel(___("Cardholder Name\n" . 'cardholder first and last name, exactly as on the card')); $name->addRule('required', ___('Please enter credit card holder name')); $name->setSeparator(' '); $name_f = $name->addText('cc_name_f', array('size'=>15, 'id' => 'cc_name_f')); $name_f->addRule('required', ___('Please enter credit card holder first name'))->addRule('regex', ___('Please enter credit card holder first name'), '|^[a-zA-Z_\' -]+$|'); $name_l = $name->addText('cc_name_l', array('size'=>15, 'id' => 'cc_name_l')); $name_l->addRule('required', ___('Please enter credit card holder last name'))->addRule('regex', ___('Please enter credit card holder last name'), '|^[a-zA-Z_\' -]+$|'); $cc = $form->addText('', array('autocomplete'=>'off', 'size'=>22, 'maxlength'=>22, 'id' => 'cc_number')) ->setLabel(___('Credit Card Number')); if ($cc_mask) $cc->setAttribute('placeholder', $cc_mask); $cc->addRule('required', ___('Please enter Credit Card Number')) ->addRule('regex', ___('Invalid Credit Card Number'), '/^[0-9 -]+$/'); class_exists('Am_Form_CreditCard', true); // preload element $expire = $form->addElement(new Am_Form_Element_CreditCardExpire('cc_expire')) ->setLabel(___("Card Expire\n" . 'Select card expiration date - month and year')); $expire->addRule('required', ___('Please enter Credit Card expiration date')); $code = $form->addPassword('', array('autocomplete'=>'off', 'size'=>4, 'maxlength'=>4, 'id' => 'cc_code')) ->setLabel(___("Credit Card Code\n". 'The "Card Code" is a three- or four-digit security code that is printed on the back of credit cards in the card\'s signature panel (or on the front for American Express cards).')); $code->addRule('required', ___('Please enter Credit Card Code')) ->addRule('regex', ___('Please enter Credit Card Code'), '/^\s*\d{3,4}\s*$/'); $fieldSet = $form->addFieldset(___('Address Info')) ->setLabel(___("Address Info\n" . '(must match your credit card statement delivery address)')); $street = $fieldSet->addText('cc_street')->setLabel(___('Street Address')) ->addRule('required', ___('Please enter Street Address')); $city = $fieldSet->addText('cc_city')->setLabel(___('City ')) ->addRule('required', ___('Please enter City')); $zip = $fieldSet->addText('cc_zip')->setLabel(___('ZIP')) ->addRule('required', ___('Please enter ZIP code')); $country = $fieldSet->addSelect('cc_country')->setLabel(___('Country')) ->setId('f_cc_country') ->loadOptions(Am_Di::getInstance()->countryTable->getOptions(true)); $country->addRule('required', ___('Please enter Country')); $group = $fieldSet->addGroup()->setLabel(___('State')); $group->addRule('required', ___('Please enter State')); /** @todo load correct states */ $stateSelect = $group->addSelect('cc_state') ->setId('f_cc_state') ->loadOptions($stateOptions = Am_Di::getInstance()->stateTable->getOptions(@$_REQUEST['cc_country'], true)); $stateText = $group->addText('cc_state')->setId('t_cc_state'); $disableObj = $stateOptions ? $stateText : $stateSelect; $disableObj->setAttribute('disabled', 'disabled')->setAttribute('style', 'display: none'); $form->addSubmit('', array('value' => $label)); $form->addHidden('id')->setValue($this->_request->get('id')); $form->addHidden('stripe_info', 'id=stripe_info')->addRule('required'); $key = json_encode($this->plugin->getConfig('public_key')); $form->addScript()->setScript(file_get_contents(AM_APPLICATION_PATH . '/default/views/public/js/json2.min.js')); $form->addScript()->setScript(<< '') return true; // submit the form! event.stopPropagation(); Stripe.setPublishableKey($key); Stripe.createToken({ number: frm.find("#cc_number").val(), cvc: frm.find("#cc_code").val(), exp_month: frm.find("[name='cc_expire[m]']").val(), exp_year: frm.find("[name='cc_expire[y]']").val(), name: frm.find("#cc_name_f").val() + " " + frm.find("#cc_name_l").val(), address_zip : frm.find("[name=cc_zip]").val(), address_line1 : frm.find("[name=cc_street]").val(), address_country : frm.find("[name=cc_country]").val(), address_city : frm.find("[name=cc_city]").val(), address_state : frm.find("[name=cc_state]").val() }, function(status, response){ // handle response if (status == '200') { frm.find("input[name=stripe_info]").val(JSON.stringify(response)); frm.submit(); } else { frm.find("input[type=submit]").prop('disabled', null); var msg; if (response.error.type == 'card_error') msg = response.error.message; else msg = 'Payment failure, please try again later'; var el = frm.find("#cc_number"); var cnt = el.closest(".element"); cnt.addClass("error"); cnt.find("span.error").remove(); el.after("
"+msg+"
"); } }); frm.find("input[type=submit]").prop('disabled', 'disabled'); return false; }); }); CUT ); $form->setDataSources(array( $this->_request, new HTML_QuickForm2_DataSource_Array($this->getDefaultValues($this->invoice->getUser())) )); return $form; } public function getDefaultValues(User $user){ return array( 'cc_name_f' => $user->name_f, 'cc_name_l' => $user->name_l, 'cc_street' => $user->street, 'cc_street2' => $user->street2, 'cc_city' => $user->city, 'cc_state' => $user->state, 'cc_country' => $user->country, 'cc_zip' => $user->zip, 'cc_phone' => $user->phone, ); } public function updateAction() { $user = $this->getDi()->user; $token = $user->data()->get(Am_Paysystem_Stripe::TOKEN); if (!$token) throw new Am_Exception_Paysystem("No credit card stored, nothing to update"); $this->invoice = $this->getDi()->invoiceTable->findFirstBy( array('user_id'=>$user->pk(), 'paysys_id'=>$this->plugin->getId()), 'invoice_id DESC'); if (!$this->invoice) throw new Am_Exception_Paysystem("No invoices found for user and paysystem"); $tr = new Am_Paysystem_Transaction_Stripe_GetCustomer($this->plugin, $this->invoice, $token); $tr->run(new Am_Paysystem_Result()); $info = $tr->getInfo(); if (empty($info['id'])) // cannot load profile { // todo delete old profile, and display cc form again! throw new Am_Exception_Paysystem("Could not load customer profile"); } if(!isset($info['active_card'])) { foreach($info['sources']['data'] as $c) if($c['id'] == $info['default_source']) $info['active_card'] = $c; } $this->form = $this->createForm(___('Update Credit Card Info'), 'XXXX XXXX XXXX ' . $info['active_card']['last4']); $n = preg_split('/\s+/', $info['active_card']['name'], 2); $this->form->addDataSource(new HTML_QuickForm2_DataSource_Array(array( 'cc_street' => $info['active_card']['address_line1'], 'cc_name_f' => $n[0], 'cc_name_l' => $n[1], 'cc_zip' => $info['active_card']['address_zip'], 'cc_expire' => sprintf('%02d%02d', $info['active_card']['exp_month'], $info['active_card']['exp_year']-2000), ))); $result = $this->ccFormAndSaveCustomer(); if ($result->isSuccess()) $this->_redirect($this->getDi()->url('member',null,false,true)); if(!$this->getInvokeArg('hosted')) { $this->form->getElementById('stripe_info')->setValue(''); $this->view->headScript()->appendFile('https://js.stripe.com/v1/'); } $this->view->title = ___('Payment info'); $this->view->display_receipt = false; $this->view->form = $this->form; $this->view->display('cc/info.phtml'); } protected function ccFormAndSaveCustomer() { $vars = $this->form->getValue(); $result = new Am_Paysystem_Result(); if(!$this->getInvokeArg('hosted')) { if (!empty($vars['stripe_info'])) { $stripe_info = json_decode($vars['stripe_info'], true); if (!$stripe_info['id']) throw new Am_Exception_Paysystem("No expected token id received"); $tr = new Am_Paysystem_Transaction_Stripe_CreateCustomer($this->plugin, $this->invoice, $stripe_info['id']); $tr->run($result); if ($result->isSuccess()) { $this->invoice->getUser()->data() ->set(Am_Paysystem_Stripe::TOKEN, $tr->getUniqId()) ->set(Am_Paysystem_Stripe::CC_EXPIRES, sprintf('%02d%02d', $stripe_info['source']['exp_month'], $stripe_info['source']['exp_year']-2000)) ->set(Am_Paysystem_Stripe::CC_MASKED, 'XXXX' . $stripe_info['source']['last4']) ->update(); // setup session to do not reask payment info within 30 minutes $s = new Zend_Session_Namespace($this->plugin->getId()); $s->setExpirationSeconds(60*30); // after 30 minutes we will reset the session $s->ccConfirmed = true; } else { $this->view->error = $result->getErrorMessages(); } } } else { if ($stripe_info = $this->_request->get('stripeToken')) { $tr = new Am_Paysystem_Transaction_Stripe_CreateCustomer($this->plugin, $this->invoice, $stripe_info); $tr->run($result); if ($result->isSuccess()) { $card = $tr->getCard(); $this->invoice->getUser()->data() ->set(Am_Paysystem_Stripe::TOKEN, $tr->getUniqId()) ->set(Am_Paysystem_Stripe::CC_EXPIRES, sprintf('%02d%02d', $card[0]['exp_month'], $card[0]['exp_year']-2000)) ->set(Am_Paysystem_Stripe::CC_MASKED, 'XXXX' . $card[0]['last4']) ->update(); // setup session to do not reask payment info within 30 minutes $s = new Zend_Session_Namespace($this->plugin->getId()); $s->setExpirationSeconds(60*30); // after 30 minutes we will reset the session $s->ccConfirmed = true; } else { $this->view->error = $result->getErrorMessages(); } } } return $result; } protected function displayReuse() { $result = new Am_Paysystem_Result; $tr = new Am_Paysystem_Transaction_Stripe_GetCustomer($this->plugin, $this->invoice, $this->invoice->getUser()->data()->get(Am_Paysystem_Stripe::TOKEN)); $tr->run($result); if (!$result->isSuccess()) throw new Am_Exception_Paysystem("Stored customer profile not found"); $card = $tr->getInfo(); $last4 = 'XXXX'; foreach($card['sources']['data'] as $c) if($c['id'] == $card['default_source']) $last4 = $c['last4']; $card = 'XXXX XXXX XXXX ' . $last4; $text = ___('Click "Continue" to pay this order using stored credit card %s', $card); $continue = ___('Continue'); $cancel = ___('Cancel'); $action = $this->plugin->getPluginUrl('cc'); $id = Am_Html::escape($this->_request->get('id')); $action = Am_Html::escape($action); $receipt = $this->view->partial('_receipt.phtml', array('invoice' => $this->invoice, 'di'=>$this->getDi())); $this->view->layoutNoMenu = true; $this->view->content .= <<

$text

   
CUT; $this->view->display('layout.phtml'); } public function ccAction() { $this->view->title = ___('Payment info'); $this->view->display_receipt = true; $this->view->layoutNoMenu = true; $this->view->invoice = $this->invoice; // if we have credit card on file, we will try to use it but we // have to display confirmation first if ($this->invoice->getUser()->data()->get(Am_Paysystem_Stripe::TOKEN)) { $s = new Zend_Session_Namespace($this->plugin->getId()); $s->setExpirationSeconds(60*30); // after 30 minutes we will reset the session //$s->ccConfirmed = !empty($s->ccConfirmed); if ($this->_request->get('reuse_ok')) { if(@$s->ccConfirmed === true) { $result = $this->plugin->doBill($this->invoice, true, $this->getDi()->CcRecordTable->createRecord()); if ($result->isSuccess()) { return $this->_redirect($this->plugin->getReturnUrl()); } else { $this->invoice->getUser()->data() ->set(Am_Paysystem_Stripe::TOKEN, null) ->set(Am_Paysystem_Stripe::CC_EXPIRES, null) ->set(Am_Paysystem_Stripe::CC_MASKED, null) ->update(); $this->view->error = $result->getErrorMessages(); $s->ccConfirmed = false; // failed } } } elseif ($this->_request->get('reuse_cancel') || (@$s->ccConfirmed === false)) { $s->ccConfirmed = false; } elseif (@$s->ccConfirmed === true) { try{ return $this->displayReuse(); }catch(Exception $e){ // Ignore it. } } } $this->form = $this->createForm(___('Subscribe And Pay')); $result = $this->ccFormAndSaveCustomer(); if ($result->isSuccess()) { $result = $this->plugin->doBill($this->invoice, true, $this->getDi()->CcRecordTable->createRecord()); if ($result->isSuccess()) { return $this->_redirect($this->plugin->getReturnUrl()); } else { $this->invoice->getUser()->data() ->set(Am_Paysystem_Stripe::TOKEN, null) ->set(Am_Paysystem_Stripe::CC_EXPIRES, null) ->set(Am_Paysystem_Stripe::CC_MASKED, null) ->update(); $this->view->error = $result->getErrorMessages(); } } if(!$this->getInvokeArg('hosted')) { $this->form->getElementById('stripe_info')->setValue(''); $this->view->headScript()->appendFile('https://js.stripe.com/v1/'); } $this->view->form = $this->form; $this->view->display('cc/info.phtml'); } } class Am_Paysystem_Transaction_Stripe extends Am_Paysystem_Transaction_CreditCard { protected $parsedResponse = array(); public function __construct(Am_Paysystem_Abstract $plugin, Invoice $invoice, $doFirst) { $request = new Am_HttpRequest('https://api.stripe.com/v1/charges', 'POST'); $amount = $doFirst ? $invoice->first_total : $invoice->second_total; $request->setAuth($plugin->getConfig('secret_key'), '') ->addPostParameter('amount', sprintf('%.02f', $amount)*100) ->addPostParameter('currency', $invoice->currency) ->addPostParameter('customer', $invoice->getUser()->data()->get(Am_Paysystem_Stripe::TOKEN)) ->addPostParameter('metadata[invoice]', $invoice->public_id) ->addPostParameter('description', 'Invoice #'.$invoice->public_id.': '.$invoice->getLineDescription()); parent::__construct($plugin, $invoice, $request, $doFirst); } public function getUniqId() { return $this->parsedResponse['id']; } public function parseResponse() { $this->parsedResponse = json_decode($this->response->getBody(), true); } public function validate() { if (@$this->parsedResponse['paid'] != 'true') { if ($this->parsedResponse['error']['type'] == 'card_error') $this->result->setErrorMessages(array($this->parsedResponse['error']['message'])); else $this->result->setErrorMessages(array(___('Payment failed'))); return false; } $this->result->setSuccess($this); return true; } } /** * Convert temporary credit card profile to customer record */ class Am_Paysystem_Transaction_Stripe_CreateCustomer extends Am_Paysystem_Transaction_CreditCard { protected $parsedResponse = array(); public function __construct(Am_Paysystem_Abstract $plugin, Invoice $invoice, $token) { $request = new Am_HttpRequest('https://api.stripe.com/v1/customers', 'POST'); $request->setAuth($plugin->getConfig('secret_key'), '') ->addPostParameter('card', $token) ->addPostParameter('email', $invoice->getEmail()) ->addPostParameter('description', 'Username:' . $invoice->getUser()->login); parent::__construct($plugin, $invoice, $request, true); } public function getUniqId() { return $this->parsedResponse['id']; } public function getCard() { return $this->parsedResponse['cards']['data']; } public function parseResponse() { $this->parsedResponse = json_decode($this->response->getBody(), true); } public function validate() { if (!@$this->parsedResponse['id']) { $code = @$this->parsedResponse['error']['code']; $message = @$this->parsedResponse['error']['message']; $error = "Error storing customer profile"; if ($code) $error .= " [".$code."]"; if ($message) $error .= " (".$message.")"; $this->result->setErrorMessages(array($error)); return false; } $this->result->setSuccess($this); return true; } public function processValidated() { } } class Am_Paysystem_Transaction_Stripe_GetCustomer extends Am_Paysystem_Transaction_CreditCard { protected $parsedResponse = array(); public function __construct(Am_Paysystem_Abstract $plugin, Invoice $invoice, $token) { $request = new Am_HttpRequest('https://api.stripe.com/v1/customers/' . $token, 'GET'); $request->setAuth($plugin->getConfig('secret_key'), ''); parent::__construct($plugin, $invoice, $request, true); } public function getUniqId() { return $this->parsedResponse['id']; } public function parseResponse() { $this->parsedResponse = json_decode($this->response->getBody(), true); } public function validate() { if (!@$this->parsedResponse['id']) $this->result->setErrorMessages(array('Unable to fetch payment profile')); $this->result->setSuccess($this); } public function getInfo() { return $this->parsedResponse; } public function processValidated() { } } class Am_Paysystem_Transaction_Stripe_Refund extends Am_Paysystem_Transaction_CreditCard { protected $parsedResponse = array(); protected $charge_id; protected $amount; public function __construct(Am_Paysystem_Abstract $plugin, Invoice $invoice, $charge_id, $amount = null) { $this->charge_id = $charge_id; $this->amount = $amount > 0 ? $amount : null; $request = new Am_HttpRequest('https://api.stripe.com/v1/charges/' . $this->charge_id . '/refund', 'POST'); $request->setAuth($plugin->getConfig('secret_key'), ''); if ($this->amount > 0) $request->addPostParameter('amount', sprintf('%.2f', $this->amount)*100); parent::__construct($plugin, $invoice, $request, true); } public function getUniqId() { $r = null; foreach ($this->parsedResponse['refunds']['data'] as $refund) { if (is_null($r) || $refund['created'] > $r['created']) { $r = $refund; } } return $r['id']; } public function parseResponse() { $this->parsedResponse = json_decode($this->response->getBody(), true); } public function validate() { if (!@$this->parsedResponse['id']) $this->result->setErrorMessages(array('Unable to fetch payment profile')); $this->result->setSuccess(); } public function processValidated() { $this->invoice->addRefund($this, $this->charge_id, $this->amount); } } class Am_Paysystem_Transaction_Stripe_Webhook extends Am_Paysystem_Transaction_Incoming { public function process() { $this->event = json_decode($this->request->getRawBody(), true); switch ($this->event['type']) { case "charge.refunded" : $r = null; $refundsList = (isset($this->event['data']['object']['refunds']['data']) ? $this->event['data']['object']['refunds']['data'] : $this->event['data']['object']['refunds']); foreach ($refundsList as $refund) { if (is_null($r) || $refund['created'] > $r['created']) { $r = $refund; } } $this->refund = $r; break; } return parent::process(); } public function validateSource() { return (bool)$this->event; } public function validateTerms() { return true; } public function validateStatus() { return true; } public function getUniqId() { switch ($this->event['type']) { case "charge.refunded" : return $this->refund['id']; default : return $this->event['id']; } } public function findInvoiceId() { return $this->event['data']['object']['metadata']['invoice']; } public function processValidated() { switch ($this->event['type']) { case "charge.refunded" : try { $this->invoice->addRefund($this, $this->event['data']['object']["id"], $this->refund['amount']/100); } catch (Am_Exception_Db_NotUnique $e) { //nop, refund is added from aMemeber admin interface } break; } } }