This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
auquotidien/auquotidien/modules/payments.py

598 lines
22 KiB
Python

import random
import string
from datetime import datetime as dt
import hashlib
import time
import urllib
from decimal import Decimal
from quixote import (redirect, get_publisher, get_request, get_session,
get_response)
from quixote.directory import Directory
from quixote.html import TemplateIO, htmltext
if not set:
from sets import Set as set
eopayment = None
try:
import eopayment
except ImportError:
pass
from wcs.qommon import _
from wcs.qommon import errors, get_logger, get_cfg, emails
from wcs.qommon.storage import StorableObject
from wcs.qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, \
WidgetDict
from wcs.qommon.misc import simplify
from wcs.formdef import FormDef
from wcs.formdata import Evolution
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
from wcs.users import User
def is_payment_supported():
if not eopayment:
return False
return get_cfg('aq-permissions', {}).get('payments', None) is not None
class Regie(StorableObject):
_names = 'regies'
label = None
description = None
service = None
service_options = None
def get_payment_object(self):
return eopayment.Payment(kind=self.service,
options=self.service_options)
class Invoice(StorableObject):
_names = 'invoices'
_hashed_indexes = ['user_id', 'regie_id']
_indexes = ['external_id']
user_id = None
regie_id = None
formdef_id = None
formdata_id = None
subject = None
details = None
amount = None
date = None
paid = False
paid_date = None
canceled = False
canceled_date = None
canceled_reason = None
next_status = None
external_id = None
request_kwargs = {}
def __init__(self, id=None, regie_id=None, formdef_id=None):
self.id = id
self.regie_id = regie_id
self.formdef_id = formdef_id
if get_publisher() and not self.id:
self.id = self.get_new_id()
def get_user(self):
if self.user_id:
return User.get(self.user_id, ignore_errors=True)
return None
@property
def username(self):
user = self.get_user()
return user.name if user else ''
def get_new_id(self, create=False):
# format : date-regie-formdef-alea-check
r = random.SystemRandom()
self.fresh = True
while True:
id = '-'.join([
dt.now().strftime('%Y%m%d'),
'r%s' % (self.regie_id or 'x'),
'f%s' % (self.formdef_id or 'x'),
''.join([r.choice(string.digits) for x in range(5)])
])
crc = '%0.2d' % (ord(hashlib.md5(id).digest()[0]) % 100)
id = id + '-' + crc
if not self.has_key(id):
return id
def store(self, *args, **kwargs):
if getattr(self, 'fresh', None) is True:
del self.fresh
notify_new_invoice(self)
return super(Invoice, self).store(*args, **kwargs)
def check_crc(cls, id):
try:
return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
except:
return False
check_crc = classmethod(check_crc)
def pay(self):
self.paid = True
self.paid_date = dt.now()
self.store()
get_logger().info(_('invoice %s paid'), self.id)
notify_paid_invoice(self)
def unpay(self):
self.paid = False
self.paid_date = None
self.store()
get_logger().info(_('invoice %s unpaid'), self.id)
def cancel(self, reason=None):
self.canceled = True
self.canceled_date = dt.now()
if reason:
self.canceled_reason = reason
self.store()
notify_canceled_invoice(self)
get_logger().info(_('invoice %s canceled'), self.id)
def payment_url(self):
base_url = get_publisher().get_frontoffice_url()
return '%s/invoices/%s' % (base_url, self.id)
INVOICE_EVO_VIEW = {
'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid with transaction number %(transaction_order_id)s'),
'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
'try': N_('Try paying invoice <a href="%(url)s">%(id)s</a> with transaction number %(transaction_order_id)s'),
}
class InvoiceEvolutionPart:
action = None
id = None
subject = None
amount = None
transaction = None
def __init__(self, action, invoice, transaction=None):
self.action = action
self.id = invoice.id
self.subject = invoice.subject
self.amount = invoice.amount
self.transaction = transaction
def view(self):
vars = {
'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
'id': self.id,
'subject': self.subject,
'amount': self.amount,
}
if self.transaction:
vars['transaction_order_id'] = self.transaction.order_id
return htmltext('<p class="invoice-%s">' % self.action + \
_(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')
class Transaction(StorableObject):
_names = 'transactions'
_hashed_indexes = ['invoice_ids']
_indexes = ['order_id']
invoice_ids = None
order_id = None
start = None
end = None
bank_data = None
def __init__(self, *args, **kwargs):
self.invoice_ids = list()
StorableObject.__init__(self, *args, **kwargs)
def get_new_id(cls, create=False):
r = random.SystemRandom()
while True:
id = ''.join([r.choice(string.digits) for x in range(16)])
if not cls.has_key(id):
return id
get_new_id = classmethod(get_new_id)
class PaymentWorkflowStatusItem(WorkflowStatusItem):
description = N_('Payment Creation')
key = 'payment'
endpoint = False
category = 'interaction'
support_substitution_variables = True
subject = None
details = None
amount = None
regie_id = None
next_status = None
request_kwargs = {}
def is_available(self, workflow=None):
return is_payment_supported()
is_available = classmethod(is_available)
def render_as_line(self):
if self.regie_id:
try:
return _('Payable to %s' % Regie.get(self.regie_id).label)
except KeyError:
return _('Payable (not completed)')
else:
return _('Payable (not completed)')
def get_parameters(self):
return ('subject', 'details', 'amount', 'regie_id', 'next_status',
'request_kwargs')
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
if 'subject' in parameters:
form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
value=self.subject, size=40)
if 'details' in parameters:
form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
value=self.details, cols=80, rows=10)
if 'amount' in parameters:
form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
if 'regie_id' in parameters:
form.add(SingleSelectWidget, '%sregie_id' % prefix,
title=_('Regie'), value=self.regie_id,
options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
if 'next_status' in parameters:
form.add(SingleSelectWidget, '%snext_status' % prefix,
title=_('Status after validation'), value = self.next_status,
hint=_('Used only if the current status of the form does not contain any "Payment Validation" item'),
options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
if 'request_kwargs' in parameters:
keys = ['name', 'email', 'address', 'phone', 'info1', 'info2', 'info3']
hint = ''
hint +=_('If the value starts by = it will be '
'interpreted as a Python expression.')
hint += ' '
hint += _('Standard keys are: %s.') % (', '.join(keys))
form.add(WidgetDict, 'request_kwargs',
title=_('Parameters for the payment system'),
hint=hint,
value = self.request_kwargs)
def perform(self, formdata):
invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
invoice.user_id = formdata.user_id
invoice.formdata_id = formdata.id
invoice.next_status = self.next_status
if self.subject:
invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
else:
invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
'form_name': formdata.formdef.name,
'formdata_id': formdata.id }
invoice.details = template_on_formdata(formdata, self.compute(self.details))
invoice.amount = Decimal(self.compute(self.amount))
invoice.date = dt.now()
invoice.request_kwargs = {}
if self.request_kwargs:
for key, value in self.request_kwargs.iteritems():
invoice.request_kwargs[key] = self.compute(value)
invoice.store()
# add a message in formdata.evolution
evo = Evolution()
evo.time = time.localtime()
evo.status = formdata.status
evo.add_part(InvoiceEvolutionPart('create', invoice))
if not formdata.evolution:
formdata.evolution = []
formdata.evolution.append(evo)
formdata.store()
# redirect the user to "my invoices"
return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
register_item_class(PaymentWorkflowStatusItem)
class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
description = N_('Payment Cancel')
key = 'payment-cancel'
endpoint = False
category = 'interaction'
reason = None
regie_id = None
def is_available(self, workflow=None):
return is_payment_supported()
is_available = classmethod(is_available)
def render_as_line(self):
if self.regie_id:
if self.regie_id == '_all':
return _('Cancel all Payments')
else:
try:
return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
except KeyError:
return _('Cancel Payments (non completed)')
else:
return _('Cancel Payments (non completed)')
def get_parameters(self):
return ('reason', 'regie_id')
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
if 'reason' in parameters:
form.add(StringWidget, '%sreason' % prefix, title=_('Reason'),
value=self.reason, size=40)
if 'regie_id' in parameters:
form.add(SingleSelectWidget, '%sregie_id' % prefix,
title=_('Regie'), value=self.regie_id,
options = [(None, '---'), ('_all', _('All Regies'))] + \
[(x.id, x.label) for x in Regie.select()])
def perform(self, formdata):
invoices_id = []
# get all invoices for the formdata and the selected regie
for evo in [evo for evo in formdata.evolution if evo.parts]:
for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
if part.action == 'create':
invoices_id.append(part.id)
elif part.id in invoices_id:
invoices_id.remove(part.id)
invoices = [Invoice.get(id) for id in invoices_id]
# select invoices for the selected regie (if not "all regies")
if self.regie_id != '_all':
invoices = [i for i in invoices if i.regie_id == self.regie_id]
# security filter: check user
invoices = [i for i in invoices if i.user_id == formdata.user_id]
# security filter: check formdata & formdef
invoices = [i for i in invoices if (i.formdata_id == formdata.id) \
and (i.formdef_id == formdata.formdef.id)]
evo = Evolution()
evo.time = time.localtime()
for invoice in invoices:
if not (invoice.paid or invoice.canceled):
invoice.cancel(self.reason)
evo.add_part(InvoiceEvolutionPart('cancel', invoice))
if not formdata.evolution:
formdata.evolution = []
formdata.evolution.append(evo)
formdata.store()
return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
register_item_class(PaymentCancelWorkflowStatusItem)
def request_payment(invoice_ids, url, add_regie=True):
for invoice_id in invoice_ids:
if not Invoice.check_crc(invoice_id):
raise KeyError()
invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
invoices = [ i for i in invoices if not (i.paid or i.canceled) ]
regie_ids = set([invoice.regie_id for invoice in invoices])
# Do not apply if more than one regie is used or no invoice is not paid or canceled
if len(invoices) == 0 or len(regie_ids) != 1:
url = get_publisher().get_frontoffice_url()
if get_session().user:
# FIXME: add error messages
url += '/myspace/invoices/'
return redirect(url)
if add_regie:
url = '%s%s' % (url, list(regie_ids)[0])
transaction = Transaction()
transaction.store()
transaction.invoice_ids = invoice_ids
transaction.start = dt.now()
amount = Decimal(0)
for invoice in invoices:
amount += Decimal(invoice.amount)
regie = Regie.get(invoice.regie_id)
payment = regie.get_payment_object()
# initialize request_kwargs using informations from the first invoice
# and update using current user informations
request_kwargs = getattr(invoices[0], 'request_kwargs', {})
request = get_request()
if request.user and request.user.email:
request_kwargs['email'] = request.user.email
if request.user and request.user.display_name:
request_kwargs['name'] = simplify(request.user.display_name)
(order_id, kind, data) = payment.request(amount, next_url=url, **request_kwargs)
transaction.order_id = order_id
transaction.store()
for invoice in invoices:
if invoice.formdef_id and invoice.formdata_id:
formdef = FormDef.get(invoice.formdef_id)
formdata = formdef.data_class().get(invoice.formdata_id)
evo = Evolution()
evo.time = time.localtime()
evo.status = formdata.status
evo.add_part(InvoiceEvolutionPart('try', invoice,
transaction=transaction))
if not formdata.evolution:
formdata.evolution = []
formdata.evolution.append(evo)
formdata.store()
if kind == eopayment.URL:
return redirect(data)
elif kind == eopayment.FORM:
return return_eopayment_form(data)
else:
raise NotImplementedError()
def return_eopayment_form(form):
r = TemplateIO(html=True)
r += htmltext('<html><body onload="document.payform.submit()">')
r += htmltext('<form action="%s" method="%s" name="payform">') % (form.url, form.method)
for field in form.fields:
r += htmltext('<input type="%s" name="%s" value="%s"/>') % (
field['type'],
field['name'],
field['value'])
r += htmltext('<input type="submit" name="submit" value="%s"/>') % _('Pay')
r += htmltext('</body></html>')
return r.getvalue()
class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
description = N_('Payment Validation')
key = 'payment-validation'
endpoint = False
category = 'interaction'
next_status = None
def is_available(self, workflow=None):
return is_payment_supported()
is_available = classmethod(is_available)
def render_as_line(self):
return _('Wait for payment validation')
def get_parameters(self):
return ('next_status',)
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
if 'next_status' in parameters:
form.add(SingleSelectWidget, '%snext_status' % prefix,
title=_('Status once validated'), value = self.next_status,
options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
register_item_class(PaymentValidationWorkflowStatusItem)
class PublicPaymentRegieBackDirectory(Directory):
def __init__(self, asynchronous):
self.asynchronous = asynchronous
def _q_lookup(self, component):
logger = get_logger()
request = get_request()
query_string = get_request().get_query()
if request.get_method() == 'POST' and query_string == '':
query_string = urllib.urlencode(request.form)
try:
regie = Regie.get(component)
except KeyError:
raise errors.TraversalError()
if self.asynchronous:
logger.debug('received asynchronous notification %r' % query_string)
payment = regie.get_payment_object()
payment_response = payment.response(query_string)
logger.debug('payment response %r', payment_response)
order_id = payment_response.order_id
bank_data = payment_response.bank_data
transaction = Transaction.get_on_index(order_id, 'order_id', ignore_errors=True)
if transaction is None:
raise errors.TraversalError()
commit = False
if not transaction.end:
commit = True
transaction.end = dt.now()
transaction.bank_data = bank_data
transaction.store()
if payment_response.signed and payment_response.is_paid() and commit:
logger.info('transaction %s successful, bankd_id:%s bank_data:%s' % (
order_id, payment_response.transaction_id, bank_data))
for invoice_id in transaction.invoice_ids:
# all invoices are now paid
invoice = Invoice.get(invoice_id)
invoice.pay()
# workflow for each related formdata
if invoice.formdef_id and invoice.formdata_id:
next_status = invoice.next_status
formdef = FormDef.get(invoice.formdef_id)
formdata = formdef.data_class().get(invoice.formdata_id)
wf_status = formdata.get_status()
for item in wf_status.items:
if isinstance(item, PaymentValidationWorkflowStatusItem):
next_status = item.next_status
break
if next_status is not None:
formdata.status = 'wf-%s' % next_status
evo = Evolution()
evo.time = time.localtime()
evo.status = formdata.status
evo.add_part(InvoiceEvolutionPart('pay', invoice,
transaction=transaction))
if not formdata.evolution:
formdata.evolution = []
formdata.evolution.append(evo)
formdata.store()
# performs the items of the new status
formdata.perform_workflow()
elif payment_response.is_error() and commit:
logger.info('transaction %s finished with failure, bank_data:%s' % (
order_id, bank_data))
elif commit:
logger.info('transaction %s is in intermediate state, bank_data:%s' % (
order_id, bank_data))
if payment_response.return_content != None and self.asynchronous:
get_response().set_content_type('text/plain')
return payment_response.return_content
else:
if payment_response.is_error():
# TODO: here return failure message
get_session().message = ('info', _('Payment failed'))
else:
# TODO: Here return success message
get_session().message = ('error', _('Payment succeeded'))
url = get_publisher().get_frontoffice_url()
if get_session().user:
url += '/myspace/invoices/'
return redirect(url)
class PublicPaymentDirectory(Directory):
_q_exports = ['', 'init', 'back', 'back_asynchronous']
back = PublicPaymentRegieBackDirectory(False)
back_asynchronous = PublicPaymentRegieBackDirectory(True)
def init(self):
invoice_ids = get_request().form.get('invoice_ids').split(' ')
for invoice_id in invoice_ids:
if not Invoice.check_crc(invoice_id):
raise KeyError()
url = get_publisher().get_frontoffice_url() + '/payment/back/'
return request_payment(invoice_ids, url)
def notify_new_invoice(invoice):
notify_invoice(invoice, 'payment-new-invoice-email')
def notify_paid_invoice(invoice):
notify_invoice(invoice, 'payment-invoice-paid-email')
def notify_canceled_invoice(invoice):
notify_invoice(invoice, 'payment-invoice-canceled-email')
def notify_invoice(invoice, template):
user = invoice.get_user()
assert user is not None
regie = Regie.get(id=invoice.regie_id)
emails.custom_template_email(template, {
'user': user,
'invoice': invoice,
'regie': regie,
'invoice_url': invoice.payment_url()
}, user.email, fire_and_forget = True)