Projet

Général

Profil

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

root / auquotidien / modules / payments.py @ 8b02623d

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 _
25
from qommon import errors, get_logger, get_cfg, emails
26
from qommon.storage import StorableObject
27
from qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, \
28
    WidgetDict
29
from qommon.misc import simplify
30

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

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

    
41

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

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

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

    
54

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

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

    
300
register_item_class(PaymentWorkflowStatusItem)
301

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

    
308
    reason = None
309
    regie_id = None
310

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

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

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

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

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

    
370
register_item_class(PaymentCancelWorkflowStatusItem)
371

    
372

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

    
390
    transaction = Transaction()
391
    transaction.store()
392
    transaction.invoice_ids = invoice_ids
393
    transaction.start = dt.now()
394

    
395
    amount = Decimal(0)
396
    for invoice in invoices:
397
        amount += Decimal(invoice.amount)
398

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

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

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

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

    
447

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

    
454
    next_status = None
455

    
456
    def is_available(self, workflow=None):
457
        return is_payment_supported()
458
    is_available = classmethod(is_available)
459

    
460
    def render_as_line(self):
461
        return _('Wait for payment validation')
462

    
463
    def get_parameters(self):
464
        return ('next_status',)
465

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

    
472
register_item_class(PaymentValidationWorkflowStatusItem)
473

    
474

    
475
class PublicPaymentRegieBackDirectory(Directory):
476
    def __init__(self, asynchronous):
477
        self.asynchronous = asynchronous
478

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

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

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

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

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

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

    
563
    back = PublicPaymentRegieBackDirectory(False)
564
    back_asynchronous = PublicPaymentRegieBackDirectory(True)
565

    
566

    
567
    def init(self):
568
        invoice_ids = get_request().form.get('invoice_ids').split(' ')
569

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

    
574
        url = get_publisher().get_frontoffice_url() + '/payment/back/'
575

    
576
        return request_payment(invoice_ids, url)
577

    
578
def notify_new_invoice(invoice):
579
    notify_invoice(invoice, 'payment-new-invoice-email')
580

    
581
def notify_paid_invoice(invoice):
582
    notify_invoice(invoice, 'payment-invoice-paid-email')
583

    
584
def notify_canceled_invoice(invoice):
585
    notify_invoice(invoice, 'payment-invoice-canceled-email')
586

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

    
(21-21/27)