Projet

Général

Profil

Télécharger (19,3 ko) Statistiques
| Branche: | Tag: | Révision:

root / extra / modules / payments.py @ 54a05fc5

1
import random
2
import string
3
from datetime import datetime as dt
4
import hashlib
5
import time
6
import urllib
7

    
8
from decimal import Decimal
9

    
10
from quixote import (redirect, get_publisher, get_request, get_session,
11
        get_response)
12
from quixote.directory import Directory
13

    
14
if not set:
15
    from sets import Set as set
16

    
17
eopayment = None
18
try:
19
    import eopayment
20
except ImportError:
21
    pass
22

    
23
from qommon.storage import StorableObject
24
from qommon import errors, get_logger, get_cfg
25
from qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, \
26
    WidgetDict
27

    
28
from wcs.formdef import FormDef
29
from wcs.formdata import Evolution
30
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
31
from wcs.users import User
32

    
33
def is_payment_supported():
34
    if not eopayment:
35
        return False
36
    return get_cfg('aq-permissions', {}).get('payments', None) is not None
37

    
38

    
39
class Regie(StorableObject):
40
    _names = 'regies'
41

    
42
    label = None
43
    description = None
44
    service = None
45
    service_options = None
46

    
47
    def get_payment_object(self):
48
        return eopayment.Payment(kind=self.service,
49
                                 options=self.service_options,
50
                                 logger=get_logger())
51

    
52

    
53
class Invoice(StorableObject):
54
    _names = 'invoices'
55
    _hashed_indexes = ['user_id', 'user_hash', 'regie_id']
56
    _indexes = ['external_id']
57

    
58
    user_id = None
59
    user_hash = None
60
    regie_id = None
61
    formdef_id = None
62
    formdata_id = None
63
    subject = None
64
    details = None
65
    amount = None
66
    date = None
67
    paid = False
68
    paid_date = None
69
    canceled = False
70
    canceled_date = None
71
    canceled_reason = None
72
    next_status = None
73
    external_id = None
74
    request_kwargs = {}
75

    
76
    def __init__(self, id=None, regie_id=None, formdef_id=None):
77
        self.id = id
78
        self.regie_id = regie_id
79
        self.formdef_id = formdef_id
80
        if get_publisher() and not self.id:
81
            self.id = self.get_new_id()
82

    
83
    def get_user(self):
84
        if self.user_id:
85
            return User.get(self.user_id, ignore_errors=True)
86
        return None
87

    
88
    @property
89
    def username(self):
90
        user = self.get_user()
91
        return user.name if user else ''
92

    
93
    def get_new_id(self, create=False):
94
        # format : date-regie-formdef-alea-check
95
        r = random.SystemRandom()
96
        while True:
97
            id = '-'.join([
98
                dt.now().strftime('%Y%m%d'),
99
                'r%s' % (self.regie_id or 'x'),
100
                'f%s' % (self.formdef_id or 'x'),
101
                ''.join([r.choice(string.digits) for x in range(5)])
102
                ])
103
            crc = '%0.2d' % (ord(hashlib.md5(id).digest()[0]) % 100)
104
            id = id + '-' + crc
105
            if not self.has_key(id):
106
                return id
107

    
108
    def check_crc(cls, id):
109
        try:
110
            return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
111
        except:
112
            return False
113
    check_crc = classmethod(check_crc)
114

    
115
    def cancel(self, reason=None):
116
        self.canceled = True
117
        self.canceled_date = dt.now()
118
        if reason:
119
            self.canceled_reason = reason
120
        self.store()
121

    
122

    
123
INVOICE_EVO_VIEW = {
124
    'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
125
    'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid with transaction number %(transaction_order_id)s'),
126
    'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
127
    'try': N_('Try paying invoice <a href="%(url)s">%(id)s</a> with transaction number %(transaction_order_id)s'),
128
}
129

    
130
class InvoiceEvolutionPart:
131
    action = None
132
    id = None
133
    subject = None
134
    amount = None
135
    transaction = None
136

    
137
    def __init__(self, action, invoice, transaction=None):
138
        self.action = action
139
        self.id = invoice.id
140
        self.subject = invoice.subject
141
        self.amount = invoice.amount
142
        self.transaction = transaction
143

    
144
    def view(self):
145
        vars = {
146
            'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
147
            'id': self.id,
148
            'subject': self.subject,
149
            'amount': self.amount,
150
        }
151
        if self.transaction:
152
            vars['transaction_order_id'] = self.transaction.order_id
153
        return htmltext('<p class="invoice-%s">' % self.action + \
154
                _(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')
155

    
156

    
157
class Transaction(StorableObject):
158
    _names = 'transactions'
159
    _hashed_indexes = ['invoice_ids']
160
    _indexes = ['order_id']
161

    
162
    invoice_ids = None
163

    
164
    order_id = None
165
    start = None
166
    end = None
167
    bank_data = None
168

    
169
    def __init__(self, *args, **kwargs):
170
        self.invoice_ids = list()
171
        StorableObject.__init__(self, *args, **kwargs)
172

    
173
    def get_new_id(cls, create=False):
174
        r = random.SystemRandom()
175
        while True:
176
            id = ''.join([r.choice(string.digits) for x in range(16)])
177
            if not cls.has_key(id):
178
                return id
179
    get_new_id = classmethod(get_new_id)
180

    
181
class PaymentWorkflowStatusItem(WorkflowStatusItem):
182
    description = N_('Payment Creation')
183
    key = 'payment'
184
    endpoint = False
185
    support_substitution_variables = True
186

    
187
    subject = None
188
    details = None
189
    amount = None
190
    regie_id = None
191
    next_status = None
192
    request_kwargs = {}
193

    
194
    def is_available(self):
195
        return is_payment_supported()
196
    is_available = classmethod(is_available)
197

    
198
    def render_as_line(self):
199
        if self.regie_id:
200
            return _('Payable to %s' % Regie.get(self.regie_id).label)
201
        else:
202
            return _('Payable (not completed)')
203

    
204
    def get_parameters(self):
205
        return ('subject', 'details', 'amount', 'regie_id', 'next_status',
206
                'request_kwargs')
207

    
208
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
209
        if 'subject' in parameters:
210
            form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
211
                     value=self.subject, size=40)
212
        if 'details' in parameters:
213
            form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
214
                     value=self.details, cols=80, rows=10)
215
        if 'amount' in parameters:
216
            form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
217
        if 'regie_id' in parameters:
218
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
219
                title=_('Regie'), value=self.regie_id,
220
                options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
221
        if 'next_status' in parameters:
222
            form.add(SingleSelectWidget, '%snext_status' % prefix,
223
                title=_('Status after validation'), value = self.next_status,
224
                hint=_('Used only if the current status of the form does not contain any "Payment Validation" item'),
225
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
226
        if 'request_kwargs' in parameters:
227
            keys = ['name', 'email', 'address', 'phone', 'info1', 'info2', 'info3']
228
            hint = ''
229
            hint +=_('If the value starts by = it will be '
230
                'interpreted as a Python expression.')
231
            hint += ' '
232
            hint += _('Standard keys are: %s.') % (', '.join(keys))
233
            form.add(WidgetDict, 'request_kwargs',
234
                title=_('Parameters for the payment system'),
235
                hint=hint,
236
                value = self.request_kwargs)
237

    
238
    def perform(self, formdata):
239
        invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
240
        invoice.user_id = formdata.user_id
241
        invoice.user_hash = formdata.user_hash
242
        invoice.formdata_id = formdata.id
243
        invoice.next_status = self.next_status
244
        if self.subject:
245
            invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
246
        else:
247
            invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
248
                    'form_name': formdata.formdef.name,
249
                    'formdata_id': formdata.id }
250
        invoice.details = template_on_formdata(formdata, self.compute(self.details))
251
        invoice.amount = Decimal(self.compute(self.amount))
252
        invoice.date = dt.now()
253
        invoice.request_kwargs = {}
254
        for key, value in self.request_kwargs.iteritems():
255
            invoice.request_kwargs[key] = self.compute(value)
256
        invoice.store()
257
        # add a message in formdata.evolution
258
        evo = Evolution()
259
        evo.time = time.localtime()
260
        evo.status = formdata.status
261
        evo.add_part(InvoiceEvolutionPart('create', invoice))
262
        if not formdata.evolution:
263
            formdata.evolution = []
264
        formdata.evolution.append(evo)
265
        formdata.store()
266
        # redirect the user to "my invoices"
267
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
268

    
269
register_item_class(PaymentWorkflowStatusItem)
270

    
271
class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
272
    description = N_('Payment Cancel')
273
    key = 'payment-cancel'
274
    endpoint = False
275

    
276
    reason = None
277
    regie_id = None
278

    
279
    def is_available(self):
280
        return is_payment_supported()
281
    is_available = classmethod(is_available)
282

    
283
    def render_as_line(self):
284
        if self.regie_id:
285
            if self.regie_id == '_all':
286
                return _('Cancel all Payments')
287
            else:
288
                return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
289
        else:
290
            return _('Cancel Payments (non completed)')
291

    
292
    def get_parameters(self):
293
        return ('reason', 'regie_id')
294

    
295
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
296
        if 'reason' in parameters:
297
            form.add(StringWidget, '%sreason' % prefix, title=_('Reason'),
298
                     value=self.reason, size=40)
299
        if 'regie_id' in parameters:
300
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
301
                title=_('Regie'), value=self.regie_id,
302
                options = [(None, '---'), ('_all', _('All Regies'))] + \
303
                            [(x.id, x.label) for x in Regie.select()])
304

    
305
    def perform(self, formdata):
306
        invoices_id = []
307
        # get all invoices for the formdata and the selected regie
308
        for evo in [evo for evo in formdata.evolution if evo.parts]:
309
            for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
310
                if part.action == 'create':
311
                    invoices_id.append(part.id)
312
                elif part.id in invoices_id:
313
                    invoices_id.remove(part.id)
314
        invoices = [Invoice.get(id) for id in invoices_id]
315
        # select invoices for the selected regie (if not "all regies")
316
        if self.regie_id != '_all':
317
            invoices = [i for i in invoices if i.regie_id == self.regie_id]
318
        # security filter: check user 
319
        invoices = [i for i in invoices if (i.user_id == formdata.user_id) \
320
                or (i.user_hash == formdata.user_hash)]
321
        # security filter: check formdata & formdef
322
        invoices = [i for i in invoices if (i.formdata_id == formdata.id) \
323
                and (i.formdef_id == formdata.formdef.id)]
324
        evo = Evolution()
325
        evo.time = time.localtime()
326
        for invoice in invoices:
327
            if not (invoice.paid or invoice.canceled):
328
                invoice.cancel(self.reason)
329
                evo.add_part(InvoiceEvolutionPart('cancel', invoice))
330
        if not formdata.evolution:
331
            formdata.evolution = []
332
        formdata.evolution.append(evo)
333
        formdata.store()
334
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
335

    
336
register_item_class(PaymentCancelWorkflowStatusItem)
337

    
338

    
339
def request_payment(invoice_ids, url, add_regie=True):
340
    for invoice_id in invoice_ids:
341
        if not Invoice.check_crc(invoice_id):
342
            raise KeyError()
343
    invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
344
    invoices = [ i for i in invoices if not (i.paid or i.canceled) ]
345
    regie_ids = set([invoice.regie_id for invoice in invoices])
346
    # Do not apply if more than one regie is used or no invoice is not paid or canceled
347
    if len(invoices) == 0 or len(regie_ids) != 1:
348
        url = get_publisher().get_frontoffice_url()
349
        if get_session().user:
350
            # FIXME: add error messages
351
            url += '/myspace/invoices/'
352
        return redirect(url)
353
    if add_regie:
354
        url = '%s%s' % (url, list(regie_ids)[0])
355

    
356
    transaction = Transaction()
357
    transaction.store()
358
    transaction.invoice_ids = invoice_ids
359
    transaction.start = dt.now()
360

    
361
    amount = Decimal(0)
362
    for invoice in invoices:
363
        amount += Decimal(invoice.amount)
364

    
365
    regie = Regie.get(invoice.regie_id)
366
    payment = regie.get_payment_object()
367
    # initialize request_kwargs using informations from the first invoice
368
    # and update using current user informations
369
    request_kwargs = getattr(invoices[0], 'request_kwargs', {})
370
    request = get_request()
371
    if request.user and request.user.email:
372
        request_kwargs['email'] = request.user.email
373
    if request.user and request.user.display_name:
374
        request_kwargs['name'] = request.user.display_name
375
    (order_id, kind, data) = payment.request(amount, next_url=url, **request_kwargs)
376
    transaction.order_id = order_id
377
    transaction.store()
378

    
379
    for invoice in invoices:
380
        if invoice.formdef_id and invoice.formdata_id:
381
            formdef = FormDef.get(invoice.formdef_id)
382
            formdata = formdef.data_class().get(invoice.formdata_id)
383
            evo = Evolution()
384
            evo.time = time.localtime()
385
            evo.status = formdata.status
386
            evo.add_part(InvoiceEvolutionPart('try', invoice,
387
                transaction=transaction))
388
            if not formdata.evolution:
389
                formdata.evolution = []
390
            formdata.evolution.append(evo)
391
            formdata.store()
392

    
393
    if kind == eopayment.URL:
394
        return redirect(data)
395
    elif kind == eopayment.FORM:
396
        raise NotImplementedError()
397
    else:
398
        raise NotImplementedError()
399

    
400

    
401
class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
402
    description = N_('Payment Validation')
403
    key = 'payment-validation'
404
    endpoint = False
405

    
406
    next_status = None
407

    
408
    def is_available(self):
409
        return is_payment_supported()
410
    is_available = classmethod(is_available)
411

    
412
    def render_as_line(self):
413
        return _('Wait for payment validation')
414

    
415
    def get_parameters(self):
416
        return ('next_status',)
417

    
418
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
419
        if 'next_status' in parameters:
420
            form.add(SingleSelectWidget, '%snext_status' % prefix,
421
                title=_('Status once validated'), value = self.next_status,
422
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
423

    
424
register_item_class(PaymentValidationWorkflowStatusItem)
425

    
426

    
427
class PublicPaymentRegieBackDirectory(Directory):
428
    def __init__(self, asynchronous):
429
        self.asynchronous = asynchronous
430

    
431
    def _q_lookup(self, component):
432
        logger = get_logger()
433
        request = get_request()
434
        query_string = get_request().get_query()
435
        if request.get_method() == 'POST' and query_string == '':
436
            query_string = urllib.urlencode(request.form)
437
        try:
438
            regie = Regie.get(component)
439
        except KeyError:
440
            raise errors.TraversalError()
441
        if self.asynchronous:
442
            logger.debug('received asynchronous notification %r' % query_string)
443
        payment = regie.get_payment_object()
444
        payment_response = payment.response(query_string)
445
        logger.debug('payment response %r', payment_response)
446
        order_id = payment_response.order_id
447
        bank_data = payment_response.bank_data
448

    
449
        transaction = Transaction.get_on_index(order_id, 'order_id', ignore_errors=True)
450
        if transaction is None:
451
            raise errors.TraversalError()
452
        commit = False
453
        if not transaction.end:
454
            commit = True
455
            transaction.end = dt.now()
456
            transaction.bank_data = bank_data
457
            transaction.store()
458
        if payment_response.signed and payment_response.is_paid() and commit:
459
            logger.info('transaction %s successful, bankd_id:%s bank_data:%s' % (
460
                                    order_id, payment_response.transaction_id, bank_data))
461

    
462
            for invoice_id in transaction.invoice_ids:
463
                # all invoices are now paid
464
                invoice = Invoice.get(invoice_id)
465
                invoice.paid = True
466
                invoice.paid_date = dt.now()
467
                invoice.store()
468

    
469
                # workflow for each related formdata
470
                if invoice.formdef_id and invoice.formdata_id:
471
                    next_status = invoice.next_status
472
                    formdef = FormDef.get(invoice.formdef_id)
473
                    formdata = formdef.data_class().get(invoice.formdata_id)
474
                    wf_status = formdata.get_workflow_status()
475
                    for item in wf_status.items:
476
                        if isinstance(item, PaymentValidationWorkflowStatusItem):
477
                            next_status = item.next_status
478
                            break
479
                    if next_status is not None:
480
                        formdata.status = 'wf-%s' % next_status
481
                        evo = Evolution()
482
                        evo.time = time.localtime()
483
                        evo.status = formdata.status
484
                        evo.add_part(InvoiceEvolutionPart('pay', invoice,
485
                            transaction=transaction))
486
                        if not formdata.evolution:
487
                            formdata.evolution = []
488
                        formdata.evolution.append(evo)
489
                        formdata.store()
490
                        # performs the items of the new status
491
                        formdata.perform_workflow()
492

    
493
        elif payment_response.is_error() and commit:
494
            logger.error('transaction %s finished with failure, bank_data:%s' % (
495
                                    order_id, bank_data))
496
        elif commit:
497
            logger.info('transaction %s is in intermediate state, bank_data:%s' % (
498
                                    order_id, bank_data))
499
        if payment_response.return_content != None and self.asynchronous:
500
            get_response().set_content_type('text/plain')
501
            return payment_response.return_content
502
        else:
503
            if payment_response.is_error():
504
                # TODO: here return failure message
505
                get_session().message = ('info', _('Payment failed'))
506
            else:
507
                # TODO: Here return success message
508
                get_session().message = ('error', _('Payment succeeded'))
509
            url = get_publisher().get_frontoffice_url()
510
            if get_session().user:
511
                url += '/myspace/invoices/'
512
            return redirect(url)
513

    
514
class PublicPaymentDirectory(Directory):
515
    _q_exports = ['', 'init', 'back', 'back_asynchronous']
516

    
517
    back = PublicPaymentRegieBackDirectory(False)
518
    back_asynchronous = PublicPaymentRegieBackDirectory(True)
519

    
520

    
521
    def init(self):
522
        invoice_ids = get_request().form.get('invoice_ids').split(' ')
523

    
524
        for invoice_id in invoice_ids:
525
            if not Invoice.check_crc(invoice_id):
526
                raise KeyError()
527

    
528
        url = get_publisher().get_frontoffice_url() + '/payment/back/'
529

    
530
        return request_payment(invoice_ids, url)
(26-26/32)