Projet

Général

Profil

Télécharger (21,7 ko) Statistiques
| Branche: | Tag: | Révision:

root / extra / modules / payments.py @ 8881779a

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
from quixote.html import TemplateIO, htmltext
14

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

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

    
24
from qommon import errors, get_logger, get_cfg, emails
25
from qommon.storage import StorableObject
26
from qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, \
27
    WidgetDict
28
from qommon.misc import simplify
29

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

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

    
40

    
41
class Regie(StorableObject):
42
    _names = 'regies'
43

    
44
    label = None
45
    description = None
46
    service = None
47
    service_options = None
48

    
49
    def get_payment_object(self):
50
        return eopayment.Payment(kind=self.service,
51
                                 options=self.service_options)
52

    
53

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

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

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

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

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

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

    
110
    def store(self, *args, **kwargs):
111
        if getattr(self, 'fresh', None) is True:
112
            del self.fresh
113
            notify_new_invoice(self)
114
        return super(Invoice, self).store(*args, **kwargs)
115

    
116
    def check_crc(cls, id):
117
        try:
118
            return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
119
        except:
120
            return False
121
    check_crc = classmethod(check_crc)
122

    
123
    def pay(self):
124
        self.paid = True
125
        self.paid_date = dt.now()
126
        self.store()
127
        get_logger().info(_('invoice %s paid'), self.id)
128
        notify_paid_invoice(self)
129

    
130
    def unpay(self):
131
        self.paid = False
132
        self.paid_date = None
133
        self.store()
134
        get_logger().info(_('invoice %s unpaid'), self.id)
135

    
136
    def cancel(self, reason=None):
137
        self.canceled = True
138
        self.canceled_date = dt.now()
139
        if reason:
140
            self.canceled_reason = reason
141
        self.store()
142
        notify_canceled_invoice(self)
143
        get_logger().info(_('invoice %s canceled'), self.id)
144

    
145
    def payment_url(self):
146
        base_url = get_publisher().get_frontoffice_url()
147
        return '%s/invoices/%s' % (base_url, self.id)
148

    
149

    
150
INVOICE_EVO_VIEW = {
151
    'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
152
    'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid with transaction number %(transaction_order_id)s'),
153
    'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
154
    'try': N_('Try paying invoice <a href="%(url)s">%(id)s</a> with transaction number %(transaction_order_id)s'),
155
}
156

    
157
class InvoiceEvolutionPart:
158
    action = None
159
    id = None
160
    subject = None
161
    amount = None
162
    transaction = None
163

    
164
    def __init__(self, action, invoice, transaction=None):
165
        self.action = action
166
        self.id = invoice.id
167
        self.subject = invoice.subject
168
        self.amount = invoice.amount
169
        self.transaction = transaction
170

    
171
    def view(self):
172
        vars = {
173
            'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
174
            'id': self.id,
175
            'subject': self.subject,
176
            'amount': self.amount,
177
        }
178
        if self.transaction:
179
            vars['transaction_order_id'] = self.transaction.order_id
180
        return htmltext('<p class="invoice-%s">' % self.action + \
181
                _(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')
182

    
183

    
184
class Transaction(StorableObject):
185
    _names = 'transactions'
186
    _hashed_indexes = ['invoice_ids']
187
    _indexes = ['order_id']
188

    
189
    invoice_ids = None
190

    
191
    order_id = None
192
    start = None
193
    end = None
194
    bank_data = None
195

    
196
    def __init__(self, *args, **kwargs):
197
        self.invoice_ids = list()
198
        StorableObject.__init__(self, *args, **kwargs)
199

    
200
    def get_new_id(cls, create=False):
201
        r = random.SystemRandom()
202
        while True:
203
            id = ''.join([r.choice(string.digits) for x in range(16)])
204
            if not cls.has_key(id):
205
                return id
206
    get_new_id = classmethod(get_new_id)
207

    
208
class PaymentWorkflowStatusItem(WorkflowStatusItem):
209
    description = N_('Payment Creation')
210
    key = 'payment'
211
    endpoint = False
212
    category = ('aq-payment', N_('Payment'))
213
    support_substitution_variables = True
214

    
215
    subject = None
216
    details = None
217
    amount = None
218
    regie_id = None
219
    next_status = None
220
    request_kwargs = {}
221

    
222
    def is_available(self, workflow=None):
223
        return is_payment_supported()
224
    is_available = classmethod(is_available)
225

    
226
    def render_as_line(self):
227
        if self.regie_id:
228
            try:
229
                return _('Payable to %s' % Regie.get(self.regie_id).label)
230
            except KeyError:
231
                return _('Payable (not completed)')
232
        else:
233
            return _('Payable (not completed)')
234

    
235
    def get_parameters(self):
236
        return ('subject', 'details', 'amount', 'regie_id', 'next_status',
237
                'request_kwargs')
238

    
239
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
240
        if 'subject' in parameters:
241
            form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
242
                     value=self.subject, size=40)
243
        if 'details' in parameters:
244
            form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
245
                     value=self.details, cols=80, rows=10)
246
        if 'amount' in parameters:
247
            form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
248
        if 'regie_id' in parameters:
249
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
250
                title=_('Regie'), value=self.regie_id,
251
                options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
252
        if 'next_status' in parameters:
253
            form.add(SingleSelectWidget, '%snext_status' % prefix,
254
                title=_('Status after validation'), value = self.next_status,
255
                hint=_('Used only if the current status of the form does not contain any "Payment Validation" item'),
256
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
257
        if 'request_kwargs' in parameters:
258
            keys = ['name', 'email', 'address', 'phone', 'info1', 'info2', 'info3']
259
            hint = ''
260
            hint +=_('If the value starts by = it will be '
261
                'interpreted as a Python expression.')
262
            hint += ' '
263
            hint += _('Standard keys are: %s.') % (', '.join(keys))
264
            form.add(WidgetDict, 'request_kwargs',
265
                title=_('Parameters for the payment system'),
266
                hint=hint,
267
                value = self.request_kwargs)
268

    
269
    def perform(self, formdata):
270
        invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
271
        invoice.user_id = formdata.user_id
272
        invoice.user_hash = formdata.user_hash
273
        invoice.formdata_id = formdata.id
274
        invoice.next_status = self.next_status
275
        if self.subject:
276
            invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
277
        else:
278
            invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
279
                    'form_name': formdata.formdef.name,
280
                    'formdata_id': formdata.id }
281
        invoice.details = template_on_formdata(formdata, self.compute(self.details))
282
        invoice.amount = Decimal(self.compute(self.amount))
283
        invoice.date = dt.now()
284
        invoice.request_kwargs = {}
285
        if self.request_kwargs:
286
            for key, value in self.request_kwargs.iteritems():
287
                invoice.request_kwargs[key] = self.compute(value)
288
        invoice.store()
289
        # add a message in formdata.evolution
290
        evo = Evolution()
291
        evo.time = time.localtime()
292
        evo.status = formdata.status
293
        evo.add_part(InvoiceEvolutionPart('create', invoice))
294
        if not formdata.evolution:
295
            formdata.evolution = []
296
        formdata.evolution.append(evo)
297
        formdata.store()
298
        # redirect the user to "my invoices"
299
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
300

    
301
register_item_class(PaymentWorkflowStatusItem)
302

    
303
class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
304
    description = N_('Payment Cancel')
305
    key = 'payment-cancel'
306
    endpoint = False
307
    category = ('aq-payment', N_('Payment'))
308

    
309
    reason = None
310
    regie_id = None
311

    
312
    def is_available(self, workflow=None):
313
        return is_payment_supported()
314
    is_available = classmethod(is_available)
315

    
316
    def render_as_line(self):
317
        if self.regie_id:
318
            if self.regie_id == '_all':
319
                return _('Cancel all Payments')
320
            else:
321
                try:
322
                    return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
323
                except KeyError:
324
                    return _('Cancel Payments (non completed)')
325
        else:
326
            return _('Cancel Payments (non completed)')
327

    
328
    def get_parameters(self):
329
        return ('reason', 'regie_id')
330

    
331
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
332
        if 'reason' in parameters:
333
            form.add(StringWidget, '%sreason' % prefix, title=_('Reason'),
334
                     value=self.reason, size=40)
335
        if 'regie_id' in parameters:
336
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
337
                title=_('Regie'), value=self.regie_id,
338
                options = [(None, '---'), ('_all', _('All Regies'))] + \
339
                            [(x.id, x.label) for x in Regie.select()])
340

    
341
    def perform(self, formdata):
342
        invoices_id = []
343
        # get all invoices for the formdata and the selected regie
344
        for evo in [evo for evo in formdata.evolution if evo.parts]:
345
            for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
346
                if part.action == 'create':
347
                    invoices_id.append(part.id)
348
                elif part.id in invoices_id:
349
                    invoices_id.remove(part.id)
350
        invoices = [Invoice.get(id) for id in invoices_id]
351
        # select invoices for the selected regie (if not "all regies")
352
        if self.regie_id != '_all':
353
            invoices = [i for i in invoices if i.regie_id == self.regie_id]
354
        # security filter: check user 
355
        invoices = [i for i in invoices if (i.user_id == formdata.user_id) \
356
                or (i.user_hash == formdata.user_hash)]
357
        # security filter: check formdata & formdef
358
        invoices = [i for i in invoices if (i.formdata_id == formdata.id) \
359
                and (i.formdef_id == formdata.formdef.id)]
360
        evo = Evolution()
361
        evo.time = time.localtime()
362
        for invoice in invoices:
363
            if not (invoice.paid or invoice.canceled):
364
                invoice.cancel(self.reason)
365
                evo.add_part(InvoiceEvolutionPart('cancel', invoice))
366
        if not formdata.evolution:
367
            formdata.evolution = []
368
        formdata.evolution.append(evo)
369
        formdata.store()
370
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
371

    
372
register_item_class(PaymentCancelWorkflowStatusItem)
373

    
374

    
375
def request_payment(invoice_ids, url, add_regie=True):
376
    for invoice_id in invoice_ids:
377
        if not Invoice.check_crc(invoice_id):
378
            raise KeyError()
379
    invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
380
    invoices = [ i for i in invoices if not (i.paid or i.canceled) ]
381
    regie_ids = set([invoice.regie_id for invoice in invoices])
382
    # Do not apply if more than one regie is used or no invoice is not paid or canceled
383
    if len(invoices) == 0 or len(regie_ids) != 1:
384
        url = get_publisher().get_frontoffice_url()
385
        if get_session().user:
386
            # FIXME: add error messages
387
            url += '/myspace/invoices/'
388
        return redirect(url)
389
    if add_regie:
390
        url = '%s%s' % (url, list(regie_ids)[0])
391

    
392
    transaction = Transaction()
393
    transaction.store()
394
    transaction.invoice_ids = invoice_ids
395
    transaction.start = dt.now()
396

    
397
    amount = Decimal(0)
398
    for invoice in invoices:
399
        amount += Decimal(invoice.amount)
400

    
401
    regie = Regie.get(invoice.regie_id)
402
    payment = regie.get_payment_object()
403
    # initialize request_kwargs using informations from the first invoice
404
    # and update using current user informations
405
    request_kwargs = getattr(invoices[0], 'request_kwargs', {})
406
    request = get_request()
407
    if request.user and request.user.email:
408
        request_kwargs['email'] = request.user.email
409
    if request.user and request.user.display_name:
410
        request_kwargs['name'] = simplify(request.user.display_name)
411
    (order_id, kind, data) = payment.request(amount, next_url=url, **request_kwargs)
412
    transaction.order_id = order_id
413
    transaction.store()
414

    
415
    for invoice in invoices:
416
        if invoice.formdef_id and invoice.formdata_id:
417
            formdef = FormDef.get(invoice.formdef_id)
418
            formdata = formdef.data_class().get(invoice.formdata_id)
419
            evo = Evolution()
420
            evo.time = time.localtime()
421
            evo.status = formdata.status
422
            evo.add_part(InvoiceEvolutionPart('try', invoice,
423
                transaction=transaction))
424
            if not formdata.evolution:
425
                formdata.evolution = []
426
            formdata.evolution.append(evo)
427
            formdata.store()
428

    
429
    if kind == eopayment.URL:
430
        return redirect(data)
431
    elif kind == eopayment.FORM:
432
        return return_eopayment_form(data)
433
    else:
434
        raise NotImplementedError()
435

    
436
def return_eopayment_form(form):
437
    r = TemplateIO(html=True)
438
    r += htmltext('<html><body onload="document.payform.submit()">')
439
    r += htmltext('<form action="%s" method="%s" name="payform">') % (form.url, form.method)
440
    for field in form.fields:
441
        r += htmltext('<input type="%s" name="%s" value="%s"/>') % (
442
                field['type'],
443
                field['name'],
444
                field['value'])
445
    r += htmltext('<input type="submit" name="submit" value="%s"/>') % _('Pay')
446
    r += htmltext('</body></html>')
447
    return r.getvalue()
448

    
449

    
450
class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
451
    description = N_('Payment Validation')
452
    key = 'payment-validation'
453
    endpoint = False
454
    category = ('aq-payment', N_('Payment'))
455

    
456
    next_status = None
457

    
458
    def is_available(self, workflow=None):
459
        return is_payment_supported()
460
    is_available = classmethod(is_available)
461

    
462
    def render_as_line(self):
463
        return _('Wait for payment validation')
464

    
465
    def get_parameters(self):
466
        return ('next_status',)
467

    
468
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
469
        if 'next_status' in parameters:
470
            form.add(SingleSelectWidget, '%snext_status' % prefix,
471
                title=_('Status once validated'), value = self.next_status,
472
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
473

    
474
register_item_class(PaymentValidationWorkflowStatusItem)
475

    
476

    
477
class PublicPaymentRegieBackDirectory(Directory):
478
    def __init__(self, asynchronous):
479
        self.asynchronous = asynchronous
480

    
481
    def _q_lookup(self, component):
482
        logger = get_logger()
483
        request = get_request()
484
        query_string = get_request().get_query()
485
        if request.get_method() == 'POST' and query_string == '':
486
            query_string = urllib.urlencode(request.form)
487
        try:
488
            regie = Regie.get(component)
489
        except KeyError:
490
            raise errors.TraversalError()
491
        if self.asynchronous:
492
            logger.debug('received asynchronous notification %r' % query_string)
493
        payment = regie.get_payment_object()
494
        payment_response = payment.response(query_string)
495
        logger.debug('payment response %r', payment_response)
496
        order_id = payment_response.order_id
497
        bank_data = payment_response.bank_data
498

    
499
        transaction = Transaction.get_on_index(order_id, 'order_id', ignore_errors=True)
500
        if transaction is None:
501
            raise errors.TraversalError()
502
        commit = False
503
        if not transaction.end:
504
            commit = True
505
            transaction.end = dt.now()
506
            transaction.bank_data = bank_data
507
            transaction.store()
508
        if payment_response.signed and payment_response.is_paid() and commit:
509
            logger.info('transaction %s successful, bankd_id:%s bank_data:%s' % (
510
                                    order_id, payment_response.transaction_id, bank_data))
511

    
512
            for invoice_id in transaction.invoice_ids:
513
                # all invoices are now paid
514
                invoice = Invoice.get(invoice_id)
515
                invoice.pay()
516

    
517
                # workflow for each related formdata
518
                if invoice.formdef_id and invoice.formdata_id:
519
                    next_status = invoice.next_status
520
                    formdef = FormDef.get(invoice.formdef_id)
521
                    formdata = formdef.data_class().get(invoice.formdata_id)
522
                    wf_status = formdata.get_status()
523
                    for item in wf_status.items:
524
                        if isinstance(item, PaymentValidationWorkflowStatusItem):
525
                            next_status = item.next_status
526
                            break
527
                    if next_status is not None:
528
                        formdata.status = 'wf-%s' % next_status
529
                        evo = Evolution()
530
                        evo.time = time.localtime()
531
                        evo.status = formdata.status
532
                        evo.add_part(InvoiceEvolutionPart('pay', invoice,
533
                            transaction=transaction))
534
                        if not formdata.evolution:
535
                            formdata.evolution = []
536
                        formdata.evolution.append(evo)
537
                        formdata.store()
538
                        # performs the items of the new status
539
                        formdata.perform_workflow()
540

    
541
        elif payment_response.is_error() and commit:
542
            logger.error('transaction %s finished with failure, bank_data:%s' % (
543
                                    order_id, bank_data))
544
        elif commit:
545
            logger.info('transaction %s is in intermediate state, bank_data:%s' % (
546
                                    order_id, bank_data))
547
        if payment_response.return_content != None and self.asynchronous:
548
            get_response().set_content_type('text/plain')
549
            return payment_response.return_content
550
        else:
551
            if payment_response.is_error():
552
                # TODO: here return failure message
553
                get_session().message = ('info', _('Payment failed'))
554
            else:
555
                # TODO: Here return success message
556
                get_session().message = ('error', _('Payment succeeded'))
557
            url = get_publisher().get_frontoffice_url()
558
            if get_session().user:
559
                url += '/myspace/invoices/'
560
            return redirect(url)
561

    
562
class PublicPaymentDirectory(Directory):
563
    _q_exports = ['', 'init', 'back', 'back_asynchronous']
564

    
565
    back = PublicPaymentRegieBackDirectory(False)
566
    back_asynchronous = PublicPaymentRegieBackDirectory(True)
567

    
568

    
569
    def init(self):
570
        invoice_ids = get_request().form.get('invoice_ids').split(' ')
571

    
572
        for invoice_id in invoice_ids:
573
            if not Invoice.check_crc(invoice_id):
574
                raise KeyError()
575

    
576
        url = get_publisher().get_frontoffice_url() + '/payment/back/'
577

    
578
        return request_payment(invoice_ids, url)
579

    
580
def notify_new_invoice(invoice):
581
    notify_invoice(invoice, 'payment-new-invoice-email')
582

    
583
def notify_paid_invoice(invoice):
584
    notify_invoice(invoice, 'payment-invoice-paid-email')
585

    
586
def notify_canceled_invoice(invoice):
587
    notify_invoice(invoice, 'payment-invoice-canceled-email')
588

    
589
def notify_invoice(invoice, template):
590
    user = invoice.get_user()
591
    assert user is not None
592
    regie = Regie.get(id=invoice.regie_id)
593
    emails.custom_ezt_email(template, { 
594
                'user': user,
595
                'invoice': invoice,
596
                'regie': regie, 
597
                'invoice_url': invoice.payment_url() 
598
            }, user.email, fire_and_forget = True)
599

    
(21-21/27)