Projet

Général

Profil

Télécharger (16,4 ko) Statistiques
| Branche: | Tag: | Révision:

root / extra / modules / payments.py @ c6ba8333

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

    
7
from decimal import Decimal
8

    
9
from quixote import redirect, get_publisher, get_request, get_session
10
from quixote.directory import Directory
11

    
12
if not set:
13
    from sets import Set as set
14

    
15
try:
16
    import eopayment
17
except ImportError:
18
    eopayment = None
19

    
20
from qommon.storage import StorableObject
21
from qommon.form import *
22
from qommon import errors, get_logger
23

    
24
from wcs.formdef import FormDef
25
from wcs.formdata import Evolution
26
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
27

    
28
def is_payment_supported():
29
    if not eopayment:
30
        return False
31
    return get_cfg('aq-permissions', {}).get('payments', None) is not None
32

    
33

    
34
class Regie(StorableObject):
35
    _names = 'regies'
36

    
37
    label = None
38
    description = None
39
    service = None
40
    service_options = None
41

    
42
    def get_payment_object(self):
43
        return eopayment.Payment(kind=self.service,
44
                                 options=self.service_options,
45
                                 logger=get_logger())
46

    
47

    
48
class Invoice(StorableObject):
49
    _names = 'invoices'
50
    _hashed_indexes = ['user_id', 'user_hash', 'regie_id']
51

    
52
    user_id = None
53
    user_hash = None
54
    regie_id = None
55
    formdef_id = None
56
    formdata_id = None
57
    subject = None
58
    details = None
59
    amount = None
60
    date = None
61
    paid = False
62
    paid_date = None
63
    canceled = False
64
    canceled_date = None
65
    canceled_reason = None
66
    next_status = None
67

    
68
    def __init__(self, id=None, regie_id=None, formdef_id=None):
69
        self.id = id
70
        self.regie_id = regie_id
71
        self.formdef_id = formdef_id
72
        if get_publisher() and not self.id:
73
            self.id = self.get_new_id()
74

    
75
    def get_new_id(self, create=False):
76
        # format : date-regie-formdef-alea-check
77
        r = random.SystemRandom()
78
        while True:
79
            id = '-'.join([
80
                dt.now().strftime('%Y%m%d'),
81
                'r%s' % (self.regie_id or 'x'),
82
                'f%s' % (self.formdef_id or 'x'),
83
                ''.join([r.choice(string.digits) for x in range(5)])
84
                ])
85
            crc = '%0.2d' % (ord(hashlib.md5(id).digest()[0]) % 100)
86
            id = id + '-' + crc
87
            if not self.has_key(id):
88
                return id
89

    
90
    def check_crc(cls, id):
91
        try:
92
            return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
93
        except:
94
            return False
95
    check_crc = classmethod(check_crc)
96

    
97
    def cancel(self, reason=None):
98
        self.canceled = True
99
        self.canceled_date = dt.now()
100
        if reason:
101
            self.canceled_reason = reason
102
        self.store()
103

    
104

    
105
INVOICE_EVO_VIEW = {
106
    'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
107
    'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid'),
108
    'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
109
}
110

    
111
class InvoiceEvolutionPart:
112
    action = None
113
    id = None
114
    subject = None
115
    amount = None
116

    
117
    def __init__(self, action, invoice):
118
        self.action = action
119
        self.id = invoice.id
120
        self.subject = invoice.subject
121
        self.amount = invoice.amount
122

    
123
    def view(self):
124
        vars = {
125
            'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
126
            'id': self.id,
127
            'subject': self.subject,
128
            'amount': self.amount,
129
        }
130
        return htmltext('<p class="invoice-%s">' % self.action + \
131
                _(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')
132

    
133

    
134
class Transaction(StorableObject):
135
    _names = 'transactions'
136
    _hashed_indexes = ['invoice_ids']
137
    _indexes = ['order_id']
138

    
139
    invoice_ids = None
140

    
141
    order_id = None
142
    start = None
143
    end = None
144
    bank_data = None
145

    
146
    def __init__(self, *args, **kwargs):
147
        self.invoice_ids = list()
148
        StorableObject.__init__(self, *args, **kwargs)
149

    
150
    def get_new_id(cls, create=False):
151
        r = random.SystemRandom()
152
        while True:
153
            id = ''.join([r.choice(string.digits) for x in range(16)])
154
            if not cls.has_key(id):
155
                return id
156
    get_new_id = classmethod(get_new_id)
157

    
158
class PaymentWorkflowStatusItem(WorkflowStatusItem):
159
    description = N_('Payment Creation')
160
    key = 'payment'
161
    endpoint = False
162
    support_substitution_variables = True
163

    
164
    subject = None
165
    details = None
166
    amount = None
167
    regie_id = None
168
    next_status = None
169

    
170
    def render_as_line(self):
171
        if self.regie_id:
172
            return _('Payable to %s' % Regie.get(self.regie_id).label)
173
        else:
174
            return _('Payable (not completed)')
175

    
176
    def get_parameters(self):
177
        return ('subject', 'details', 'amount', 'regie_id', 'next_status')
178

    
179
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
180
        if 'subject' in parameters:
181
            form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
182
                     value=self.subject, size=40)
183
        if 'details' in parameters:
184
            form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
185
                     value=self.details, cols=80, rows=10)
186
        if 'amount' in parameters:
187
            form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
188
        if 'regie_id' in parameters:
189
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
190
                title=_('Regie'), value=self.regie_id,
191
                options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
192
        if 'next_status' in parameters:
193
            form.add(SingleSelectWidget, '%snext_status' % prefix,
194
                title=_('Status after validation'), value = self.next_status,
195
                hint=_('Used only if the current status of the form does not contain any "Payment Validation" item'),
196
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
197

    
198
    def calculate_amount(self, formdata):
199
        if not self.amount:
200
            return 0
201
        if not self.amount.startswith('='):
202
            return self.amount
203
        vars = get_publisher().substitutions.get_context_variables()
204
        vars['Decimal'] = Decimal
205
        amount = eval(self.amount[1:], vars)
206
        # XXX: catch and report the error somehow
207
        return amount
208

    
209
    def perform(self, formdata):
210
        invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
211
        invoice.user_id = formdata.user_id
212
        invoice.user_hash = formdata.user_hash
213
        invoice.formdata_id = formdata.id
214
        invoice.next_status = self.next_status
215
        if self.subject:
216
            invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
217
        else:
218
            invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
219
                    'form_name': formdata.formdef.name,
220
                    'formdata_id': formdata.id }
221
        invoice.details = template_on_formdata(formdata, self.compute(self.details))
222
        invoice.amount = self.calculate_amount(formdata)
223
        invoice.date = dt.now()
224
        invoice.store()
225
        # add a message in formdata.evolution
226
        evo = Evolution()
227
        evo.time = time.localtime()
228
        evo.status = formdata.status
229
        evo.add_part(InvoiceEvolutionPart('create', invoice))
230
        if not formdata.evolution:
231
            formdata.evolution = []
232
        formdata.evolution.append(evo)
233
        formdata.store()
234
        # redirect the user to "my invoices"
235
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
236

    
237
register_item_class(PaymentWorkflowStatusItem)
238

    
239
class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
240
    description = N_('Payment Cancel')
241
    key = 'payment-cancel'
242
    endpoint = False
243

    
244
    reason = None
245
    regie_id = None
246

    
247
    def render_as_line(self):
248
        if self.regie_id:
249
            if self.regie_id == '_all':
250
                return _('Cancel all Payments')
251
            else:
252
                return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
253
        else:
254
            return _('Cancel Payments (non completed)')
255

    
256
    def get_parameters(self):
257
        return ('reason', 'regie_id')
258

    
259
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
260
        if 'reason' in parameters:
261
            form.add(StringWidget, '%sreason' % prefix, title=_('Reason'),
262
                     value=self.reason, size=40)
263
        if 'regie_id' in parameters:
264
            form.add(SingleSelectWidget, '%sregie_id' % prefix,
265
                title=_('Regie'), value=self.regie_id,
266
                options = [(None, '---'), ('_all', _('All Regies'))] + \
267
                            [(x.id, x.label) for x in Regie.select()])
268

    
269
    def perform(self, formdata):
270
        invoices_id = []
271
        # get all invoices for the formdata and the selected regie
272
        for evo in [evo for evo in formdata.evolution if evo.parts]:
273
            for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
274
                if part.action == 'create':
275
                    invoices_id.append(part.id)
276
                elif part.id in invoices_id:
277
                    invoices_id.remove(part.id)
278
        invoices = [Invoice.get(id) for id in invoices_id]
279
        # select invoices for the selected regie (if not "all regies")
280
        if self.regie_id != '_all':
281
            invoices = [i for i in invoices if i.regie_id == self.regie_id]
282
        # security filter: check user 
283
        invoices = [i for i in invoices if (i.user_id == formdata.user_id) \
284
                or (i.user_hash == formdata.user_hash)]
285
        # security filter: check formdata & formdef
286
        invoices = [i for i in invoices if (i.formdata_id == formdata.id) \
287
                and (i.formdef_id == formdata.formdef.id)]
288
        evo = Evolution()
289
        evo.time = time.localtime()
290
        for invoice in invoices:
291
            if not (invoice.paid or invoice.canceled):
292
                invoice.cancel(self.reason)
293
                evo.add_part(InvoiceEvolutionPart('cancel', invoice))
294
        if not formdata.evolution:
295
            formdata.evolution = []
296
        formdata.evolution.append(evo)
297
        formdata.store()
298
        return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
299

    
300
register_item_class(PaymentCancelWorkflowStatusItem)
301

    
302

    
303
def request_payment(invoice_ids, url, add_regie=True):
304
    for invoice_id in invoice_ids:
305
        if not Invoice.check_crc(invoice_id):
306
            raise KeyError()
307
    invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
308
    invoices = [ i for i in invoices if not (i.paid or i.canceled) ]
309
    regie_ids = set([invoice.regie_id for invoice in invoices])
310
    # Do not apply if more than one regie is used or no invoice is not paid or canceled
311
    if len(invoices) == 0 or len(regie_ids) != 1:
312
        url = get_publisher().get_frontoffice_url()
313
        if get_session().user:
314
            # FIXME: add error messages
315
            url += '/myspace/invoices/'
316
        return redirect(url)
317
    if add_regie:
318
        url = '%s%s' % (url, list(regie_ids)[0])
319

    
320
    transaction = Transaction()
321
    transaction.store()
322
    transaction.invoice_ids = invoice_ids
323
    transaction.start = dt.now()
324

    
325
    amount = Decimal(0)
326
    for invoice in invoices:
327
        amount += Decimal(invoice.amount)
328

    
329
    regie = Regie.get(invoice.regie_id)
330
    payment = regie.get_payment_object()
331
    (order_id, kind, data) = payment.request(amount, next_url=url)
332
    transaction.order_id = order_id
333
    transaction.store()
334

    
335
    if kind == eopayment.URL:
336
        return redirect(data)
337
    elif kind == eopayment.FORM:
338
        raise NotImplementedError()
339
    else:
340
        raise NotImplementedError()
341

    
342

    
343
class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
344
    description = N_('Payment Validation')
345
    key = 'payment-validation'
346
    endpoint = False
347

    
348
    next_status = None
349

    
350
    def render_as_line(self):
351
        return _('Wait for payment validation')
352

    
353
    def get_parameters(self):
354
        return ('next_status',)
355

    
356
    def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
357
        if 'next_status' in parameters:
358
            form.add(SingleSelectWidget, '%snext_status' % prefix,
359
                title=_('Status once validated'), value = self.next_status,
360
                options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
361

    
362
register_item_class(PaymentValidationWorkflowStatusItem)
363

    
364

    
365
class PublicPaymentRegieBackDirectory(Directory):
366
    def __init__(self, asynchronous):
367
        self.asynchronous = asynchronous
368

    
369
    def _q_lookup(self, component):
370
        logger = get_logger()
371
        try:
372
            regie = Regie.get(component)
373
        except KeyError:
374
            raise errors.TraversalError()
375
        query_string = get_request().get_query()
376
        if self.asynchronous:
377
            logger.debug('received asynchronous notification %r' % query_string)
378
        payment = regie.get_payment_object()
379
        payment_response = payment.response(query_string)
380
        logger.debug('payment response %r', payment_response)
381
        order_id = payment_response.order_id
382
        bank_data = payment_response.bank_data
383
        return_content = payment_response.return_content
384

    
385
        if payment_response.signed and payment_response.is_paid():
386
            logger.info('transaction %s successful, bankd_id:%s bank_data:%s' % (
387
                                    order_id, payment_response.transaction_id, bank_data))
388
            transaction = Transaction.get_on_index(order_id, 'order_id')
389
            transaction.end = dt.now()
390
            transaction.bank_data = bank_data
391
            transaction.store()
392

    
393
            for invoice_id in transaction.invoice_ids:
394
                # all invoices are now paid
395
                invoice = Invoice.get(invoice_id)
396
                invoice.paid = True
397
                invoice.paid_date = dt.now()
398
                invoice.store()
399

    
400
                # workflow for each related formdata
401
                if invoice.formdef_id and invoice.formdata_id:
402
                    next_status = invoice.next_status
403
                    formdef = FormDef.get(invoice.formdef_id)
404
                    formdata = formdef.data_class().get(invoice.formdata_id)
405
                    wf_status = formdata.get_workflow_status()
406
                    for item in wf_status.items:
407
                        if isinstance(item, PaymentValidationWorkflowStatusItem):
408
                            next_status = item.next_status
409
                            break
410
                    if next_status is not None:
411
                        formdata.status = 'wf-%s' % next_status
412
                        evo = Evolution()
413
                        evo.time = time.localtime()
414
                        evo.status = formdata.status
415
                        if not formdata.evolution:
416
                            formdata.evolution = []
417
                        formdata.evolution.append(evo)
418
                        formdata.store()
419
                        # performs the items of the new status
420
                        formdata.perform_workflow()
421

    
422
        elif payment_response.is_error():
423
            logger.error('transaction %s finished with failure, bank_data:%s' % (
424
                                    order_id, bank_data))
425
        else:
426
            logger.info('transaction %s is in intermediate state, bank_data:%s' % (
427
                                    order_id, bank_data))
428
        if payment_response.return_content != None and self.asynchronous:
429
            get_response().set_content_type('text/plain')
430
            return payment_response.return_content
431
        else:
432
            if payment_response.is_error():
433
                # TODO: here return failure message
434
                get_session().message = ('info', _('Payment failed'))
435
            else:
436
                # TODO: Here return success message
437
                get_session().message = ('error', _('Payment succeeded'))
438
            url = get_publisher().get_frontoffice_url()
439
            if get_session().user:
440
                url += '/myspace/invoices/'
441
            return redirect(url)
442

    
443
class PublicPaymentDirectory(Directory):
444
    _q_exports = ['', 'init', 'back', 'back_asynchronous']
445

    
446
    back = PublicPaymentRegieBackDirectory(False)
447
    back_asynchronous = PublicPaymentRegieBackDirectory(True)
448

    
449

    
450
    def init(self):
451
        invoice_ids = get_request().form.get('invoice_ids').split(' ')
452

    
453
        for invoice_id in invoice_ids:
454
            if not Invoice.check_crc(invoice_id):
455
                raise KeyError()
456

    
457
        url = get_publisher().get_frontoffice_url() + '/payment/back/'
458

    
459
        return request_payment(invoice_ids, url)
(20-20/26)