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;
}
}
}