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

602 lines
22 KiB
Python

import random
import string
from datetime import datetime as dt
import hashlib
import time
from decimal import Decimal
from django.utils.six.moves.urllib import parse as urllib
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 _, N_
from wcs.qommon import force_str
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
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 get_publisher().user_class.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' % (hashlib.md5(id.encode('utf-8')).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:]) == (hashlib.md5(id[:-3].encode('utf-8')).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 not self.action:
return ''
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.items():
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 errors.QueryError()
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(force_str(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">') % (force_str(form.url), force_str(form.method))
for field in form.fields:
r += htmltext('<input type="%s" name="%s" value="%s"/>') % (
force_str(field['type']),
force_str(field['name']),
force_str(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):
if 'invoice_ids' not in get_request().form:
raise errors.QueryError()
invoice_ids = get_request().form.get('invoice_ids').split(' ')
for invoice_id in invoice_ids:
if not Invoice.check_crc(invoice_id):
raise errors.QueryError()
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)