[ADD] payment_stripe

https://stripe.com/docs/api

Closes #11587
This commit is contained in:
Martin Trigaux
2016-04-04 17:09:08 +02:00
parent 8b596e8e1b
commit 33c094d218
18 changed files with 641 additions and 0 deletions
+16
View File
@@ -112,4 +112,20 @@
</ul>
</field>
</record>
<record id="payment_acquirer_stripe" model="payment.acquirer">
<field name="name">Stripe</field>
<field name="view_template_id" ref="default_acquirer_button"/>
<field name="image" type="base64" file="payment_stripe/static/description/icon.png"/>
<field name="module_id" ref="base.module_payment_stripe"/>
<field name="description" type="html">
<p>
A payment gateway to accept online payments via credit cards.
</p>
<ul>
<li><i class="fa fa-check"/>eCommerce</li>
</ul>
</field>
</record>
</odoo>
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import models
import controllers
+19
View File
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
{
'name': 'Stripe Payment Acquirer',
'category': 'Hidden',
'summary': 'Payment Acquirer: Stripe Implementation',
'version': '1.0',
'description': """Stripe Payment Acquirer""",
'depends': ['payment'],
'data': [
'views/stripe.xml',
'views/payment_acquirer.xml',
'views/payment_stripe_template.xml',
'data/payment_stripe_data.xml',
],
'images': ['static/description/icon.png'],
'installable': True,
'license': 'OEEL-1',
}
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import main
+51
View File
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
import logging
import pprint
import werkzeug
from odoo import http
from odoo.http import request
_logger = logging.getLogger(__name__)
class StripeController(http.Controller):
@http.route(['/payment/stripe/s2s/create_json'], type='json', auth='public')
def stripe_s2s_create_json(self, **kwargs):
acquirer_id = int(kwargs.get('acquirer_id'))
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
return acquirer.s2s_process(kwargs)
@http.route(['/payment/stripe/s2s/create'], type='http', auth='public')
def stripe_s2s_create(self, **post):
acquirer_id = int(post.get('acquirer_id'))
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
acquirer.s2s_process(post)
return werkzeug.utils.redirect(post.get('return_url', '/'))
@http.route(['/payment/stripe/create_charge'], type='json', auth='public')
def stripe_create_charge(self, **post):
""" Create a payment transaction
Expects the result from the user input from checkout.js popup"""
acquirer = request.env['payment.acquirer'].browse(int(post.get('acquirer_id')))
order = request.env['sale.order'].sudo().browse(int(request.session.get('sale_order_id')))
tx = request.env['payment.transaction'].sudo().create({
'acquirer_id': acquirer.id,
'reference': str(post.get('invoice_num')),
'amount': float(post.get('amount')),
'currency_id': request.env['res.currency'].search([('name', '=', post.get('currency'))], limit=1).id,
'partner_id': request.env.user.partner_id.id,
'sale_order_id': order.id
})
order.write({
'payment_acquirer_id': acquirer.id,
'payment_tx_id': tx.id
})
request.session['sale_transaction_id'] = tx.id
response = tx._create_stripe_charge(tokenid=post['tokenid'])
_logger.info('Stripe: entering form_feedback with post data %s', pprint.pformat(response))
if response:
request.env['payment.transaction'].sudo().form_feedback(response, 'stripe')
return post.pop('return_url', '/')
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="payment.payment_acquirer_stripe" model="payment.acquirer">
<field name="name">Stripe</field>
<field name="image" type="base64" file="payment_stripe/static/description/icon.png"/>
<field name="provider">stripe</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="stripe_acquirer_button"/>
<field name="registration_view_template_id" ref="stripe_s2s_form"/>
<field name="environment">test</field>
<field name="pre_msg"><![CDATA[
<p>You will be prompt with Stripe Payment page for payment information.</p>]]></field>
<field name="stripe_secret_key">dummy</field>
<field name="stripe_publishable_key">dummy</field>
</record>
</data>
</odoo>
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
import payment_transaction
import payment_acquirer
import payment_method
@@ -0,0 +1,68 @@
# -*- coding: utf-'8' "-*-"
from odoo import api, fields, models
# Force the API version to avoid breaking in case of update on Stripe side
# cf https://stripe.com/docs/api#versioning
# changelog https://stripe.com/docs/upgrades#api-changelog
STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'}
class PaymentAcquirerStripe(models.Model):
_inherit = 'payment.acquirer'
stripe_secret_key = fields.Char(required_if_provider='stripe')
stripe_publishable_key = fields.Char(required_if_provider='stripe')
@api.model
def _get_providers(self):
providers = super(PaymentAcquirerStripe, self)._get_providers()
providers.append(['stripe', 'Stripe'])
return providers
@api.multi
def stripe_form_generate_values(self, tx_values):
self.ensure_one()
stripe_tx_values = dict(tx_values)
temp_stripe_tx_values = {
'amount': tx_values.get('amount'),
'currency': tx_values.get('currency') and tx_values.get('currency').name or '',
'address_line1': tx_values['partner_address'],
'address_city': tx_values['partner_city'],
'address_country': tx_values['partner_country'] and tx_values['partner_country'].name or '',
'email': tx_values['partner_email'],
'address_zip': tx_values['partner_zip'],
'name': tx_values['partner_name'],
'phone': tx_values['partner_phone'],
}
temp_stripe_tx_values['returndata'] = stripe_tx_values.pop('return_url', '')
stripe_tx_values.update(temp_stripe_tx_values)
return stripe_tx_values
@api.model
def _get_stripe_api_url(self):
return 'api.stripe.com/v1'
@api.model
def stripe_s2s_form_process(self, data):
payment_method = self.env['payment.method'].sudo().create({
'cc_number': data['cc_number'],
'cc_holder_name': data['cc_holder_name'],
'cc_expiry': data['cc_expiry'],
'cc_brand': data['cc_brand'],
'cvc': data['cvc'],
'acquirer_id': int(data['acquirer_id']),
'partner_id': int(data['partner_id'])
})
return payment_method.id
@api.multi
def stripe_s2s_form_validate(self, data):
self.ensure_one()
# mandatory fields
for field_name in ["cc_number", "cvc", "cc_holder_name", "cc_expiry", "cc_brand"]:
if not data.get(field_name):
return False
return True
@@ -0,0 +1,50 @@
# -*- coding: utf-'8' "-*-"
import requests
from odoo import api, models
from odoo.exceptions import UserError
from . import payment_acquirer as pa
class PaymentMethodStripe(models.Model):
_inherit = 'payment.method'
@api.model
def stripe_create(self, values):
res = None
payment_acquirer = self.env['payment.acquirer'].browse(values.get('acquirer_id'))
url_token = 'https://%s/tokens' % payment_acquirer._get_stripe_api_url()
url_customer = 'https://%s/customers' % payment_acquirer._get_stripe_api_url()
if values['cc_number']:
payment_params = {
'card[number]': values['cc_number'].replace(' ', ''),
'card[exp_month]': str(values['cc_expiry'][:2]),
'card[exp_year]': str(values['cc_expiry'][-2:]),
'card[cvc]': values['cvc'],
}
r = requests.post(url_token,
auth=(payment_acquirer.stripe_secret_key, ''),
params=payment_params,
headers=pa.STRIPE_HEADERS)
token = r.json()
if token.get('id'):
customer_params = {
'source': token['id']
}
r = requests.post(url_customer,
auth=(payment_acquirer.stripe_secret_key, ''),
params=customer_params,
headers=pa.STRIPE_HEADERS)
customer = r.json()
res = {
'acquirer_ref': customer['id'],
'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name'])
}
elif token.get('error'):
raise UserError(token['error']['message'])
# pop credit card info to info sent to create
for field_name in ["cc_number", "cvc", "cc_holder_name", "cc_expiry", "cc_brand"]:
values.pop(field_name)
return res
@@ -0,0 +1,101 @@
# -*- coding: utf-'8' "-*-"
import json
import logging
import requests
import urllib
from odoo import api, fields, models, _
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.tools.safe_eval import safe_eval
from . import payment_acquirer as pa
_logger = logging.getLogger(__name__)
class PaymentTransactionStripe(models.Model):
_inherit = 'payment.transaction'
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None):
api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url())
charge_params = {
'amount': int(self.amount*100), # Stripe takes amount in cents (https://support.stripe.com/questions/which-zero-decimal-currencies-does-stripe-support)
'currency': self.currency_id.name,
'metadata[reference]': self.reference
}
if acquirer_ref:
charge_params['customer'] = acquirer_ref
if tokenid:
charge_params['card'] = str(tokenid)
r = requests.post(api_url_charge,
auth=(self.acquirer_id.stripe_secret_key, ''),
params=charge_params,
headers=pa.STRIPE_HEADERS)
return r.json()
@api.multi
def stripe_s2s_do_transaction(self, **kwargs):
self.ensure_one()
result = self._create_stripe_charge(acquirer_ref=self.payment_method_id.acquirer_ref)
return self._stripe_s2s_validate_tree(result)
@api.model
def _stripe_form_get_tx_from_data(self, data):
""" Given a data dict coming from stripe, verify it and find the related
transaction record. """
reference = data['metadata']['reference']
if not reference:
error_msg = _('Stripe: invalid reply received from provider, missing reference')
_logger.error(error_msg, data['metadata'])
raise ValidationError(error_msg)
tx = self.search([('reference', '=', reference)])
if not tx:
error_msg = (_('Stripe: no order found for reference %s') % reference)
_logger.error(error_msg)
raise ValidationError(error_msg)
elif len(tx) > 1:
error_msg = (_('Stripe: %s orders found for reference %s') % (len(tx), reference))
_logger.error(error_msg)
raise ValidationError(error_msg)
return tx[0]
@api.multi
def _stripe_s2s_validate_tree(self, tree):
self.ensure_one()
if self.state not in ('draft', 'pending'):
_logger.info('Stripe: trying to validate an already validated tx (ref %s)', self.reference)
return True
status = tree.get('status')
if status == 'succeeded':
self.write({
'state': 'done',
'date_validate': fields.datetime.now(),
'acquirer_reference': tree.get('id'),
})
if self.callback_eval:
safe_eval(self.callback_eval, {'self': self})
return True
else:
error = tree['error']['message']
_logger.warn(error)
self.sudo().write({
'state': 'error',
'state_message': error,
'acquirer_reference': tree.get('id'),
'date_validate': fields.datetime.now(),
})
return False
@api.model
def _stripe_form_get_invalid_parameters(self, tx, data):
invalid_parameters = []
reference = data['metadata']['reference']
if reference != tx.reference:
invalid_parameters.append(('Reference', reference, tx.reference))
return invalid_parameters
@api.model
def _stripe_form_validate(self, tx, data):
return tx._stripe_s2s_validate_tree(data)
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,39 @@
odoo.define('payment_stripe.stripe', function(require) {
"use strict";
var ajax = require('web.ajax');
var handler = StripeCheckout.configure({
key: $("input[name='stripe_key']").val(),
locale: 'auto',
closed: function() {
if (!handler.isTokenGenerate) {
$('#pay_stripe')
.removeAttr('disabled')
.find('i').remove();
}
},
token: function(token, args) {
handler.isTokenGenerate = true;
ajax.jsonRpc("/payment/stripe/create_charge", 'call', {
tokenid: token.id,
email: token.email,
amount: $("input[name='amount']").val(),
acquirer_id: $("#acquirer_stripe").val(),
currency: $("input[name='currency']").val(),
invoice_num: $("input[name='invoice_num']").val(),
return_url: $("input[name='return_url']").val()
}).done(function(data){
handler.isTokenGenerate = false;
window.location.href = data;
});
},
});
$('#pay_stripe').on('click', function(e) {
// Open Checkout with further options
if(!$(this).find('i').length)
$(this).append('<i class="fa fa-spinner fa-spin"/>');
$(this).attr('disabled','disabled');
handler.open();
e.preventDefault();
});
});
+2
View File
@@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
import test_stripe
+174
View File
@@ -0,0 +1,174 @@
# -*- coding: utf-8 -*-
import odoo
from odoo import fields
from odoo.addons.payment.tests.common import PaymentAcquirerCommon
from odoo.tools import mute_logger
@odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True)
class StripeCommon(PaymentAcquirerCommon):
def setUp(self):
super(StripeCommon, self).setUp()
self.currency_euro = self.env['res.currency'].search([('name', '=', 'EUR')], limit=1)
# get the stripe account
self.stripe_id = self.env.ref('payment.payment_acquirer_stripe').id
@odoo.tests.common.at_install(True)
@odoo.tests.common.post_install(True)
class StripeTest(StripeCommon):
def test_10_stripe_s2s(self):
stripe = self.env['payment.acquirer'].browse(self.stripe_id)
self.assertEqual(stripe.environment, 'test', 'test without test environment')
# Add Stripe credentials
stripe.write({
'stripe_secret_key': 'sk_test_bldAlqh1U24L5HtRF9mBFpK7',
'stripe_publishable_key': 'pk_test_0TKSyYSZS9AcS4keZ2cxQQCW',
})
# Create payment meethod for Stripe
payment_method = self.env['payment.method'].create({
'acquirer_id': self.stripe_id,
'partner_id': self.buyer_id,
'cc_number': '4242424242424242',
'cc_expiry': '02 / 26',
'cc_brand': 'visa',
'cvc': '111',
'cc_holder_name': 'Johndoe',
})
# Create transaction
tx = self.env['payment.transaction'].create({
'reference': 'test_ref_%s' % fields.date.today(),
'currency_id': self.currency_euro.id,
'acquirer_id': self.stripe_id,
'partner_id': self.buyer_id,
'payment_method_id': payment_method.id,
'type': 'server2server',
'amount': 115.0
})
tx.stripe_s2s_do_transaction()
# Check state
self.assertEqual(tx.state, 'done', 'Stripe: Transcation has been discarded.')
def test_20_stripe_form_render(self):
stripe = self.env['payment.acquirer'].browse(self.stripe_id)
self.assertEqual(stripe.environment, 'test', 'test without test environment')
# ----------------------------------------
# Test: button direct rendering
# ----------------------------------------
form_values = {
'amount': 320.0,
'currency': 'EUR',
'address_line1': 'Huge Street 2/543',
'address_city': 'Sin City',
'address_country': 'Belgium',
'email': 'norbert.buyer@example.com',
'address_zip': '1000',
'name': 'Norbert Buyer',
'phone': '0032 12 34 56 78'
}
# render the button
res = stripe.render('SO404', 320.0, self.currency_euro.id, values=self.buyer_values)
post_url = "https://checkout.stripe.com/checkout.js"
email = "norbert.buyer@example.com"
# check form result
if "https://checkout.stripe.com/checkout.js" in res[0]:
self.assertEqual(post_url, 'https://checkout.stripe.com/checkout.js', 'Stripe: wrong form POST url')
# Generated and received
if email in res[0]:
self.assertEqual(
email, form_values.get('email'),
'Stripe: wrong value for input %s: received %s instead of %s' % (email, email, form_values.get('email'))
)
def test_30_stripe_form_management(self):
stripe = self.env['payment.acquirer'].browse(self.stripe_id)
self.assertEqual(stripe.environment, 'test', 'test without test environment')
# typical data posted by Stripe after client has successfully paid
stripe_post_data = {
u'amount': 4700,
u'amount_refunded': 0,
u'application_fee': None,
u'balance_transaction': u'txn_172xfnGMfVJxozLwssrsQZyT',
u'captured': True,
u'created': 1446529775,
u'currency': u'eur',
u'customer': None,
u'description': None,
u'destination': None,
u'dispute': None,
u'failure_code': None,
u'failure_message': None,
u'fraud_details': {},
u'id': u'ch_172xfnGMfVJxozLwEjSfpfxD',
u'invoice': None,
u'livemode': False,
u'metadata': {u'reference': u'SO100'},
u'object': u'charge',
u'paid': True,
u'receipt_email': None,
u'receipt_number': None,
u'refunded': False,
u'refunds': {u'data': [],
u'has_more': False,
u'object': u'list',
u'total_count': 0,
u'url': u'/v1/charges/ch_172xfnGMfVJxozLwEjSfpfxD/refunds'},
u'shipping': None,
u'source': {u'address_city': None,
u'address_country': None,
u'address_line1': None,
u'address_line1_check': None,
u'address_line2': None,
u'address_state': None,
u'address_zip': None,
u'address_zip_check': None,
u'brand': u'Visa',
u'country': u'US',
u'customer': None,
u'cvc_check': u'pass',
u'dynamic_last4': None,
u'exp_month': 2,
u'exp_year': 2022,
u'fingerprint': u'9tJA9bUEuvEb3Ell',
u'funding': u'credit',
u'id': u'card_172xfjGMfVJxozLw1QO6gYNM',
u'last4': u'4242',
u'metadata': {},
u'name': u'norbert.buyer@example.com',
u'object': u'card',
u'tokenization_method': None},
u'statement_descriptor': None,
u'status': u'succeeded'}
tx = self.env['payment.transaction'].create({
'amount': 4700,
'acquirer_id': self.stripe_id,
'currency_id': self.currency_euro.id,
'reference': 'SO100',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france_id})
# validate it
tx.form_feedback(stripe_post_data, 'stripe')
self.assertEqual(tx.state, 'done', 'Stripe: validation did not put tx into done state')
self.assertEqual(tx.acquirer_reference, stripe_post_data.get('id'), 'Stripe: validation did not update tx id')
# reset tx
tx.write({'state': 'draft', 'date_validate': False, 'acquirer_reference': False})
# simulate an error
stripe_post_data['status'] = 'error'
stripe_post_data.update({u'error': {u'message': u"Your card's expiration year is invalid.", u'code': u'invalid_expiry_year', u'type': u'card_error', u'param': u'exp_year'}})
with mute_logger('openerp.addons.payment_stripe.models.payment_transaction'):
tx.form_feedback(stripe_post_data, 'stripe')
# check state
self.assertEqual(tx.state, 'error', 'Stipe: erroneous validation did not put tx into error state')
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="acquirer_form_stripe" model="ir.ui.view">
<field name="name">payment.acquirer.form.inherit</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='after'>
<group attrs="{'invisible': [('provider', '!=', 'stripe')]}">
<field name="stripe_secret_key" password="True"/>
<field name="stripe_publishable_key" password="True"/>
</group>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="payment_stripe_redirect" name="Payment Stripe">
<script type="text/javascript">
window.location.href = '<t t-esc="return_url"/>';
</script>
</template>
</odoo>
+66
View File
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="stripe_acquirer_button">
<form t-if="acquirer" method="post" target="_self">
<script src="https://checkout.stripe.com/checkout.js"></script>
<input type='hidden' name='amount' t-att-value="amount"/>
<input type="hidden" name='return_url' t-att-value='returndata'/>
<!--Order Information -->
<input type='hidden' name='invoice_num' t-att-value='reference'/>
<input type='hidden' name='currency' t-att-value='currency'/>
<!-- Billing Information-->
<input type='hidden' name='name' t-att-value='name'/>
<input type='hidden' name='address_line1' t-att-value='address_line1'/>
<input type='hidden' name='address_city' t-att-value='address_city'/>
<input type='hidden' name='address_zip' t-att-value='address_zip'/>
<input type='hidden' name='address_country' t-att-value='address_country'/>
<input type='hidden' name='phone' t-att-value='phone'/>
<input type='hidden' name='email' t-att-value='email'/>
<input id="acquirer_stripe" type='hidden' name="acquirer" t-att-value="acquirer.id"/>
<input type='hidden' name="stripe_key" t-att-value="acquirer.stripe_publishable_key"/>
<!-- Submit-->
<button id="pay_stripe" width="100px" t-att-class="submit_class">
<img t-if="not submit_txt" src="/payment_stripe/static/src/img/stripe_icon.png"/>
<span t-if="submit_txt"><t t-esc="submit_txt"/> <span class="fa fa-long-arrow-right"/></span>
</button>
<script type="text/javascript" src="/payment_stripe/static/src/js/stripe.js"></script>
</form>
</template>
<template id="stripe_s2s_form">
<form method="post" t-att-action="'/payment/stripe/s2s/create' if not json else '/payment/stripe/s2s/create_json'">
<div t-att-class="'row' if bootstrap_formatting else ''">
<div t-att-class="'form-group col-md-8' if bootstrap_formatting else 'form-group'">
<label class="control-label" for="cc_number">Card Number</label>
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;"/>
<div class="card_placeholder"></div>
<div class="visa"></div>
<input type="hidden" name="cc_brand" value=""/>
</div>
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
<label class="control-label" for="cvc">CVC</label>
<input type="text" name="cvc" id="cvc" class="form-control" maxlength="4" placeholder="CVC"/>
</div>
<div t-att-class="'form-group col-md-7' if bootstrap_formatting else 'form-group'">
<label class="control-label" for="cc_holder_name">Card Holder Name</label>
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control"/>
</div>
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
<label class="control-label" for="cc_expires_mm">Expiration Date</label>
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="MM / YY"/>
</div>
<input type="hidden" name="acquirer_id" t-att-value="id"/>
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="clearfix"/>
</div>
<button t-att-class="submit_class" class="btn btn-primary" type="submit">
<span t-if="not submit_txt">Confirm <span class="fa fa-long-arrow-right"></span></span>
<span t-if="submit_txt"><t t-esc="submit_txt"/></span>
</button>
</form>
</template>
</data>
</odoo>