Projet

Général

Profil

Télécharger (22,1 ko) Statistiques
| Branche: | Tag: | Révision:

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

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

    
7
from decimal import Decimal
8

    
9
from django.utils.six.moves.urllib import parse as urllib
10

    
11
from quixote import redirect, get_publisher, get_request, get_session, get_response
12
from quixote.directory import Directory
13
from quixote.errors import QueryError
14
from quixote.html import TemplateIO, htmltext
15

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

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

    
25
from wcs.qommon import _, N_
26
from wcs.qommon import force_str
27
from wcs.qommon import errors, get_logger, get_cfg, emails
28
from wcs.qommon.storage import StorableObject
29
from wcs.qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, WidgetDict
30
from wcs.qommon.misc import simplify
31

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

    
36

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

    
42

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

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

    
51
    def get_payment_object(self):
52
        return eopayment.Payment(kind=self.service, 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 get_publisher().user_class.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
                [
101
                    dt.now().strftime('%Y%m%d'),
102
                    'r%s' % (self.regie_id or 'x'),
103
                    'f%s' % (self.formdef_id or 'x'),
104
                    ''.join([r.choice(string.digits) for x in range(5)]),
105
                ]
106
            )
107
            crc = '%0.2d' % (hashlib.md5(id.encode('utf-8')).digest()[0] % 100)
108
            id = id + '-' + crc
109
            if not self.has_key(id):
110
                return id
111

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

    
118
    def check_crc(cls, id):
119
        try:
120
            return int(id[-2:]) == (hashlib.md5(id[:-3].encode('utf-8')).digest()[0] % 100)
121
        except:
122
            return False
123

    
124
    check_crc = classmethod(check_crc)
125

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

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

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

    
148
    def payment_url(self):
149
        base_url = get_publisher().get_frontoffice_url()
150
        return '%s/invoices/%s' % (base_url, self.id)
151

    
152

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

    
164

    
165
class InvoiceEvolutionPart(EvolutionPart):
166
    action = None
167
    id = None
168
    subject = None
169
    amount = None
170
    transaction = None
171

    
172
    def __init__(self, action, invoice, transaction=None):
173
        self.action = action
174
        self.id = invoice.id
175
        self.subject = invoice.subject
176
        self.amount = invoice.amount
177
        self.transaction = transaction
178

    
179
    def view(self):
180
        vars = {
181
            'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
182
            'id': self.id,
183
            'subject': self.subject,
184
            'amount': self.amount,
185
        }
186
        if not self.action:
187
            return ''
188
        if self.transaction:
189
            vars['transaction_order_id'] = self.transaction.order_id
190
        return htmltext(
191
            '<p class="invoice-%s">' % self.action + _(INVOICE_EVO_VIEW[self.action]) % vars + '</p>'
192
        )
193

    
194

    
195
class Transaction(StorableObject):
196
    _names = 'transactions'
197
    _hashed_indexes = ['invoice_ids']
198
    _indexes = ['order_id']
199

    
200
    invoice_ids = None
201

    
202
    order_id = None
203
    start = None
204
    end = None
205
    bank_data = None
206

    
207
    def __init__(self, *args, **kwargs):
208
        self.invoice_ids = list()
209
        StorableObject.__init__(self, *args, **kwargs)
210

    
211
    def get_new_id(cls, create=False):
212
        r = random.SystemRandom()
213
        while True:
214
            id = ''.join([r.choice(string.digits) for x in range(16)])
215
            if not cls.has_key(id):
216
                return id
217

    
218
    get_new_id = classmethod(get_new_id)
219

    
220

    
221
class PaymentWorkflowStatusItem(WorkflowStatusItem):
222
    description = N_('Payment Creation')
223
    key = 'payment'
224
    endpoint = False
225
    category = 'interaction'
226
    support_substitution_variables = True
227

    
228
    subject = None
229
    details = None
230
    amount = None
231
    regie_id = None
232
    next_status = None
233
    request_kwargs = {}
234

    
235
    def is_available(self, workflow=None):
236
        return is_payment_supported()
237

    
238
    is_available = classmethod(is_available)
239

    
240
    def render_as_line(self):
241
        if self.regie_id:
242
            try:
243
                return _('Payable to %s' % Regie.get(self.regie_id).label)
244
            except KeyError:
245
                return _('Payable (not completed)')
246
        else:
247
            return _('Payable (not completed)')
248

    
249
    def get_parameters(self):
250
        return ('subject', 'details', 'amount', 'regie_id', 'next_status', 'request_kwargs')
251

    
252
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
253
        if 'subject' in parameters:
254
            form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'), value=self.subject, size=40)
255
        if 'details' in parameters:
256
            form.add(
257
                TextWidget, '%sdetails' % prefix, title=_('Details'), value=self.details, cols=80, rows=10
258
            )
259
        if 'amount' in parameters:
260
            form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
261
        if 'regie_id' in parameters:
262
            form.add(
263
                SingleSelectWidget,
264
                '%sregie_id' % prefix,
265
                title=_('Regie'),
266
                value=self.regie_id,
267
                options=[(None, '---')] + [(x.id, x.label) for x in Regie.select()],
268
            )
269
        if 'next_status' in parameters:
270
            form.add(
271
                SingleSelectWidget,
272
                '%snext_status' % prefix,
273
                title=_('Status after validation'),
274
                value=self.next_status,
275
                hint=_(
276
                    'Used only if the current status of the form does not contain any "Payment Validation" item'
277
                ),
278
                options=[(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status],
279
            )
280
        if 'request_kwargs' in parameters:
281
            keys = ['name', 'email', 'address', 'phone', 'info1', 'info2', 'info3']
282
            hint = ''
283
            hint += _('If the value starts by = it will be ' 'interpreted as a Python expression.')
284
            hint += ' '
285
            hint += _('Standard keys are: %s.') % (', '.join(keys))
286
            form.add(
287
                WidgetDict,
288
                'request_kwargs',
289
                title=_('Parameters for the payment system'),
290
                hint=hint,
291
                value=self.request_kwargs,
292
            )
293

    
294
    def perform(self, formdata):
295
        invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
296
        invoice.user_id = formdata.user_id
297
        invoice.formdata_id = formdata.id
298
        invoice.next_status = self.next_status
299
        if self.subject:
300
            invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
301
        else:
302
            invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
303
                'form_name': formdata.formdef.name,
304
                'formdata_id': formdata.id,
305
            }
306
        invoice.details = template_on_formdata(formdata, self.compute(self.details))
307
        invoice.amount = Decimal(self.compute(self.amount))
308
        invoice.date = dt.now()
309
        invoice.request_kwargs = {}
310
        if self.request_kwargs:
311
            for key, value in self.request_kwargs.items():
312
                invoice.request_kwargs[key] = self.compute(value)
313
        invoice.store()
314
        # add a message in formdata.evolution
315
        evo = Evolution()
316
        evo.time = time.localtime()
317
        evo.status = formdata.status
318
        evo.add_part(InvoiceEvolutionPart('create', invoice))
319
        if not formdata.evolution:
320
            formdata.evolution = []
321
        formdata.evolution.append(evo)
322
        formdata.store()
323
        # redirect the user to "my invoices"
324
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
325

    
326

    
327
register_item_class(PaymentWorkflowStatusItem)
328

    
329

    
330
class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
331
    description = N_('Payment Cancel')
332
    key = 'payment-cancel'
333
    endpoint = False
334
    category = 'interaction'
335

    
336
    reason = None
337
    regie_id = None
338

    
339
    def is_available(self, workflow=None):
340
        return is_payment_supported()
341

    
342
    is_available = classmethod(is_available)
343

    
344
    def render_as_line(self):
345
        if self.regie_id:
346
            if self.regie_id == '_all':
347
                return _('Cancel all Payments')
348
            else:
349
                try:
350
                    return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
351
                except KeyError:
352
                    return _('Cancel Payments (non completed)')
353
        else:
354
            return _('Cancel Payments (non completed)')
355

    
356
    def get_parameters(self):
357
        return ('reason', 'regie_id')
358

    
359
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
360
        if 'reason' in parameters:
361
            form.add(StringWidget, '%sreason' % prefix, title=_('Reason'), value=self.reason, size=40)
362
        if 'regie_id' in parameters:
363
            form.add(
364
                SingleSelectWidget,
365
                '%sregie_id' % prefix,
366
                title=_('Regie'),
367
                value=self.regie_id,
368
                options=[(None, '---'), ('_all', _('All Regies'))]
369
                + [(x.id, x.label) for x in Regie.select()],
370
            )
371

    
372
    def perform(self, formdata):
373
        invoices_id = []
374
        # get all invoices for the formdata and the selected regie
375
        for evo in [evo for evo in formdata.evolution if evo.parts]:
376
            for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
377
                if part.action == 'create':
378
                    invoices_id.append(part.id)
379
                elif part.id in invoices_id:
380
                    invoices_id.remove(part.id)
381
        invoices = [Invoice.get(id) for id in invoices_id]
382
        # select invoices for the selected regie (if not "all regies")
383
        if self.regie_id != '_all':
384
            invoices = [i for i in invoices if i.regie_id == self.regie_id]
385
        # security filter: check user
386
        invoices = [i for i in invoices if i.user_id == formdata.user_id]
387
        # security filter: check formdata & formdef
388
        invoices = [
389
            i for i in invoices if (i.formdata_id == formdata.id) and (i.formdef_id == formdata.formdef.id)
390
        ]
391
        evo = Evolution()
392
        evo.time = time.localtime()
393
        for invoice in invoices:
394
            if not (invoice.paid or invoice.canceled):
395
                invoice.cancel(self.reason)
396
                evo.add_part(InvoiceEvolutionPart('cancel', invoice))
397
        if not formdata.evolution:
398
            formdata.evolution = []
399
        formdata.evolution.append(evo)
400
        formdata.store()
401
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
402

    
403

    
404
register_item_class(PaymentCancelWorkflowStatusItem)
405

    
406

    
407
def request_payment(invoice_ids, url, add_regie=True):
408
    for invoice_id in invoice_ids:
409
        if not Invoice.check_crc(invoice_id):
410
            raise QueryError()
411
    invoices = [Invoice.get(invoice_id) for invoice_id in invoice_ids]
412
    invoices = [i for i in invoices if not (i.paid or i.canceled)]
413
    regie_ids = set([invoice.regie_id for invoice in invoices])
414
    # Do not apply if more than one regie is used or no invoice is not paid or canceled
415
    if len(invoices) == 0 or len(regie_ids) != 1:
416
        url = get_publisher().get_frontoffice_url()
417
        if get_session().user:
418
            # FIXME: add error messages
419
            url += '/myspace/invoices/'
420
        return redirect(url)
421
    if add_regie:
422
        url = '%s%s' % (url, list(regie_ids)[0])
423

    
424
    transaction = Transaction()
425
    transaction.store()
426
    transaction.invoice_ids = invoice_ids
427
    transaction.start = dt.now()
428

    
429
    amount = Decimal(0)
430
    for invoice in invoices:
431
        amount += Decimal(invoice.amount)
432

    
433
    regie = Regie.get(invoice.regie_id)
434
    payment = regie.get_payment_object()
435
    # initialize request_kwargs using informations from the first invoice
436
    # and update using current user informations
437
    request_kwargs = getattr(invoices[0], 'request_kwargs', {})
438
    request = get_request()
439
    if request.user and request.user.email:
440
        request_kwargs['email'] = request.user.email
441
    if request.user and request.user.display_name:
442
        request_kwargs['name'] = simplify(request.user.display_name)
443
    (order_id, kind, data) = payment.request(amount, next_url=url, **request_kwargs)
444
    transaction.order_id = order_id
445
    transaction.store()
446

    
447
    for invoice in invoices:
448
        if invoice.formdef_id and invoice.formdata_id:
449
            formdef = FormDef.get(invoice.formdef_id)
450
            formdata = formdef.data_class().get(invoice.formdata_id)
451
            evo = Evolution()
452
            evo.time = time.localtime()
453
            evo.status = formdata.status
454
            evo.add_part(InvoiceEvolutionPart('try', invoice, transaction=transaction))
455
            if not formdata.evolution:
456
                formdata.evolution = []
457
            formdata.evolution.append(evo)
458
            formdata.store()
459

    
460
    if kind == eopayment.URL:
461
        return redirect(force_str(data))
462
    elif kind == eopayment.FORM:
463
        return return_eopayment_form(data)
464
    else:
465
        raise NotImplementedError()
466

    
467

    
468
def return_eopayment_form(form):
469
    r = TemplateIO(html=True)
470
    r += htmltext('<html><body onload="document.payform.submit()">')
471
    r += htmltext('<form action="%s" method="%s" name="payform">') % (
472
        force_str(form.url),
473
        force_str(form.method),
474
    )
475
    for field in form.fields:
476
        r += htmltext('<input type="%s" name="%s" value="%s"/>') % (
477
            force_str(field['type']),
478
            force_str(field['name']),
479
            force_str(field['value']),
480
        )
481
    r += htmltext('<input type="submit" name="submit" value="%s"/>') % _('Pay')
482
    r += htmltext('</body></html>')
483
    return r.getvalue()
484

    
485

    
486
class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
487
    description = N_('Payment Validation')
488
    key = 'payment-validation'
489
    endpoint = False
490
    category = 'interaction'
491

    
492
    next_status = None
493

    
494
    def is_available(self, workflow=None):
495
        return is_payment_supported()
496

    
497
    is_available = classmethod(is_available)
498

    
499
    def render_as_line(self):
500
        return _('Wait for payment validation')
501

    
502
    def get_parameters(self):
503
        return ('next_status',)
504

    
505
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None, **kwargs):
506
        if 'next_status' in parameters:
507
            form.add(
508
                SingleSelectWidget,
509
                '%snext_status' % prefix,
510
                title=_('Status once validated'),
511
                value=self.next_status,
512
                options=[(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status],
513
            )
514

    
515

    
516
register_item_class(PaymentValidationWorkflowStatusItem)
517

    
518

    
519
class PublicPaymentRegieBackDirectory(Directory):
520
    def __init__(self, asynchronous):
521
        self.asynchronous = asynchronous
522

    
523
    def _q_lookup(self, component):
524
        logger = get_logger()
525
        request = get_request()
526
        query_string = get_request().get_query()
527
        if request.get_method() == 'POST' and query_string == '':
528
            query_string = urllib.urlencode(request.form)
529
        try:
530
            regie = Regie.get(component)
531
        except KeyError:
532
            raise errors.TraversalError()
533
        if self.asynchronous:
534
            logger.debug('received asynchronous notification %r' % query_string)
535
        payment = regie.get_payment_object()
536
        payment_response = payment.response(query_string)
537
        logger.debug('payment response %r', payment_response)
538
        order_id = payment_response.order_id
539
        bank_data = payment_response.bank_data
540

    
541
        transaction = Transaction.get_on_index(order_id, 'order_id', ignore_errors=True)
542
        if transaction is None:
543
            raise errors.TraversalError()
544
        commit = False
545
        if not transaction.end:
546
            commit = True
547
            transaction.end = dt.now()
548
            transaction.bank_data = bank_data
549
            transaction.store()
550
        if payment_response.signed and payment_response.is_paid() and commit:
551
            logger.info(
552
                'transaction %s successful, bankd_id:%s bank_data:%s'
553
                % (order_id, payment_response.transaction_id, bank_data)
554
            )
555

    
556
            for invoice_id in transaction.invoice_ids:
557
                # all invoices are now paid
558
                invoice = Invoice.get(invoice_id)
559
                invoice.pay()
560

    
561
                # workflow for each related formdata
562
                if invoice.formdef_id and invoice.formdata_id:
563
                    next_status = invoice.next_status
564
                    formdef = FormDef.get(invoice.formdef_id)
565
                    formdata = formdef.data_class().get(invoice.formdata_id)
566
                    wf_status = formdata.get_status()
567
                    for item in wf_status.items:
568
                        if isinstance(item, PaymentValidationWorkflowStatusItem):
569
                            next_status = item.next_status
570
                            break
571
                    if next_status is not None:
572
                        formdata.status = 'wf-%s' % next_status
573
                        evo = Evolution()
574
                        evo.time = time.localtime()
575
                        evo.status = formdata.status
576
                        evo.add_part(InvoiceEvolutionPart('pay', invoice, transaction=transaction))
577
                        if not formdata.evolution:
578
                            formdata.evolution = []
579
                        formdata.evolution.append(evo)
580
                        formdata.store()
581
                        # performs the items of the new status
582
                        formdata.perform_workflow()
583

    
584
        elif payment_response.is_error() and commit:
585
            logger.info('transaction %s finished with failure, bank_data:%s' % (order_id, bank_data))
586
        elif commit:
587
            logger.info('transaction %s is in intermediate state, bank_data:%s' % (order_id, bank_data))
588
        if payment_response.return_content != None and self.asynchronous:
589
            get_response().set_content_type('text/plain')
590
            return payment_response.return_content
591
        else:
592
            if payment_response.is_error():
593
                # TODO: here return failure message
594
                get_session().message = ('info', _('Payment failed'))
595
            else:
596
                # TODO: Here return success message
597
                get_session().message = ('error', _('Payment succeeded'))
598
            url = get_publisher().get_frontoffice_url()
599
            if get_session().user:
600
                url += '/myspace/invoices/'
601
            return redirect(url)
602

    
603

    
604
class PublicPaymentDirectory(Directory):
605
    _q_exports = ['init', 'back', 'back_asynchronous']
606

    
607
    back = PublicPaymentRegieBackDirectory(False)
608
    back_asynchronous = PublicPaymentRegieBackDirectory(True)
609

    
610
    def init(self):
611
        if 'invoice_ids' not in get_request().form:
612
            raise QueryError()
613
        invoice_ids = get_request().form.get('invoice_ids').split(' ')
614

    
615
        for invoice_id in invoice_ids:
616
            if not Invoice.check_crc(invoice_id):
617
                raise QueryError()
618

    
619
        url = get_publisher().get_frontoffice_url() + '/payment/back/'
620

    
621
        return request_payment(invoice_ids, url)
622

    
623

    
624
def notify_new_invoice(invoice):
625
    notify_invoice(invoice, 'payment-new-invoice-email')
626

    
627

    
628
def notify_paid_invoice(invoice):
629
    notify_invoice(invoice, 'payment-invoice-paid-email')
630

    
631

    
632
def notify_canceled_invoice(invoice):
633
    notify_invoice(invoice, 'payment-invoice-canceled-email')
634

    
635

    
636
def notify_invoice(invoice, template):
637
    user = invoice.get_user()
638
    assert user is not None
639
    regie = Regie.get(id=invoice.regie_id)
640
    emails.custom_template_email(
641
        template,
642
        {'user': user, 'invoice': invoice, 'regie': regie, 'invoice_url': invoice.payment_url()},
643
        user.email,
644
        fire_and_forget=True,
645
    )
(10-10/14)