Project

General

Profile

Download (20.8 KB) Statistics
| Branch: | Tag: | Revision:
cfb41dcc Frédéric Péters
import random
import string
391939ac Benjamin Dauvergne
from datetime import datetime as dt
3e1fe4cf Thomas Noël
import hashlib
fc8e3469 Benjamin Dauvergne
import time
f1ab9b59 Benjamin Dauvergne
import urllib
c43d0b77 Frédéric Péters
cfb41dcc Frédéric Péters
from decimal import Decimal

fc8e3469 Benjamin Dauvergne
from quixote import (redirect, get_publisher, get_request, get_session,
get_response)
c43d0b77 Frédéric Péters
from quixote.directory import Directory

391939ac Benjamin Dauvergne
if not set:
from sets import Set as set

fc8e3469 Benjamin Dauvergne
eopayment = None
0bd673f1 Frédéric Péters
try:
import eopayment
except ImportError:
fc8e3469 Benjamin Dauvergne
pass
0bd673f1 Frédéric Péters
90c63943 Benjamin Dauvergne
from qommon import errors, get_logger, get_cfg, emails
c43d0b77 Frédéric Péters
from qommon.storage import StorableObject
700cf8e8 Benjamin Dauvergne
from qommon.form import htmltext, StringWidget, TextWidget, SingleSelectWidget, \
WidgetDict
c43d0b77 Frédéric Péters
cfb41dcc Frédéric Péters
from wcs.formdef import FormDef
c43d0b77 Frédéric Péters
from wcs.formdata import Evolution
c8b173d6 Thomas Noël
from wcs.workflows import WorkflowStatusItem, register_item_class, template_on_formdata
bc4e32b1 Benjamin Dauvergne
from wcs.users import User
c43d0b77 Frédéric Péters
0bd673f1 Frédéric Péters
def is_payment_supported():
if not eopayment:
return False
return get_cfg('aq-permissions', {}).get('payments', None) is not None


c43d0b77 Frédéric Péters
class Regie(StorableObject):
_names = 'regies'

label = None
description = None
3c0aeb99 Frédéric Péters
service = None
service_options = None

def get_payment_object(self):
return eopayment.Payment(kind=self.service,
46bdb4f7 Benjamin Dauvergne
options=self.service_options,
logger=get_logger())
c43d0b77 Frédéric Péters

class Invoice(StorableObject):
_names = 'invoices'
c6ba8333 Thomas Noël
_hashed_indexes = ['user_id', 'user_hash', 'regie_id']
0eb3d025 Benjamin Dauvergne
_indexes = ['external_id']
c43d0b77 Frédéric Péters
user_id = None
c6ba8333 Thomas Noël
user_hash = None
c43d0b77 Frédéric Péters
regie_id = None
formdef_id = None
formdata_id = None
9d9d1ebd Thomas Noël
subject = None
c8b173d6 Thomas Noël
details = None
c43d0b77 Frédéric Péters
amount = None
date = None
paid = False
paid_date = None
c6ba8333 Thomas Noël
canceled = False
canceled_date = None
canceled_reason = None
f0a875e0 Thomas Noël
next_status = None
0eb3d025 Benjamin Dauvergne
external_id = None
700cf8e8 Benjamin Dauvergne
request_kwargs = {}
c43d0b77 Frédéric Péters
3e1fe4cf Thomas Noël
def __init__(self, id=None, regie_id=None, formdef_id=None):
self.id = id
self.regie_id = regie_id
self.formdef_id = formdef_id
if get_publisher() and not self.id:
self.id = self.get_new_id()

bc4e32b1 Benjamin Dauvergne
def get_user(self):
if self.user_id:
return User.get(self.user_id, ignore_errors=True)
return None

@property
def username(self):
user = self.get_user()
return user.name if user else ''

3e1fe4cf Thomas Noël
def get_new_id(self, create=False):
# format : date-regie-formdef-alea-check
r = random.SystemRandom()
90c63943 Benjamin Dauvergne
self.fresh = True
3e1fe4cf Thomas Noël
while True:
id = '-'.join([
dt.now().strftime('%Y%m%d'),
'r%s' % (self.regie_id or 'x'),
'f%s' % (self.formdef_id or 'x'),
''.join([r.choice(string.digits) for x in range(5)])
])
crc = '%0.2d' % (ord(hashlib.md5(id).digest()[0]) % 100)
id = id + '-' + crc
if not self.has_key(id):
return id

90c63943 Benjamin Dauvergne
def store(self, *args, **kwargs):
if getattr(self, 'fresh', None) is True:
del self.fresh
notify_new_invoice(self)
return super(Invoice, self).store(*args, **kwargs)

3e1fe4cf Thomas Noël
def check_crc(cls, id):
try:
return int(id[-2:]) == (ord(hashlib.md5(id[:-3]).digest()[0]) % 100)
except:
return False
check_crc = classmethod(check_crc)

90c63943 Benjamin Dauvergne
def pay(self):
self.paid = True
self.paid_date = dt.now()
self.store()
get_logger().info(_('invoice %s paid'), self.id)
notify_paid_invoice(self)

def unpay(self):
self.paid = False
self.paid_date = None
self.store()
get_logger().info(_('invoice %s unpaid'), self.id)

c6ba8333 Thomas Noël
def cancel(self, reason=None):
self.canceled = True
self.canceled_date = dt.now()
if reason:
self.canceled_reason = reason
self.store()
90c63943 Benjamin Dauvergne
notify_canceled_invoice(self)
get_logger().info(_('invoice %s canceled'), self.id)

def payment_url(self):
base_url = get_publisher().get_frontoffice_url()
return '%s/invoices/%s' % (base_url, self.id)
c6ba8333 Thomas Noël

INVOICE_EVO_VIEW = {
'create': N_('Create Invoice <a href="%(url)s">%(id)s</a>: %(subject)s - %(amount)s &euro;'),
56756adc Benjamin Dauvergne
'pay': N_('Invoice <a href="%(url)s">%(id)s</a> is paid with transaction number %(transaction_order_id)s'),
c6ba8333 Thomas Noël
'cancel': N_('Cancel Invoice <a href="%(url)s">%(id)s</a>'),
56756adc Benjamin Dauvergne
'try': N_('Try paying invoice <a href="%(url)s">%(id)s</a> with transaction number %(transaction_order_id)s'),
c6ba8333 Thomas Noël
}

class InvoiceEvolutionPart:
action = None
id = None
subject = None
amount = None
56756adc Benjamin Dauvergne
transaction = None
c6ba8333 Thomas Noël
56756adc Benjamin Dauvergne
def __init__(self, action, invoice, transaction=None):
c6ba8333 Thomas Noël
self.action = action
self.id = invoice.id
self.subject = invoice.subject
self.amount = invoice.amount
56756adc Benjamin Dauvergne
self.transaction = transaction
c6ba8333 Thomas Noël
def view(self):
vars = {
'url': '%s/invoices/%s' % (get_publisher().get_frontoffice_url(), self.id),
'id': self.id,
'subject': self.subject,
'amount': self.amount,
}
56756adc Benjamin Dauvergne
if self.transaction:
vars['transaction_order_id'] = self.transaction.order_id
c6ba8333 Thomas Noël
return htmltext('<p class="invoice-%s">' % self.action + \
_(INVOICE_EVO_VIEW[self.action]) % vars + '</p>')

c43d0b77 Frédéric Péters
cfb41dcc Frédéric Péters
class Transaction(StorableObject):
_names = 'transactions'
391939ac Benjamin Dauvergne
_hashed_indexes = ['invoice_ids']
f0a875e0 Thomas Noël
_indexes = ['order_id']
cfb41dcc Frédéric Péters
invoice_ids = None

f0a875e0 Thomas Noël
order_id = None
cfb41dcc Frédéric Péters
start = None
391939ac Benjamin Dauvergne
end = None
cfb41dcc Frédéric Péters
bank_data = None

391939ac Benjamin Dauvergne
def __init__(self, *args, **kwargs):
self.invoice_ids = list()
StorableObject.__init__(self, *args, **kwargs)

3e1fe4cf Thomas Noël
def get_new_id(cls, create=False):
cfb41dcc Frédéric Péters
r = random.SystemRandom()
while True:
id = ''.join([r.choice(string.digits) for x in range(16)])
3e1fe4cf Thomas Noël
if not cls.has_key(id):
cfb41dcc Frédéric Péters
return id
get_new_id = classmethod(get_new_id)

c43d0b77 Frédéric Péters
class PaymentWorkflowStatusItem(WorkflowStatusItem):
b29b079c Thomas Noël
description = N_('Payment Creation')
c43d0b77 Frédéric Péters
key = 'payment'
endpoint = False
315a95c4 Frédéric Péters
category = ('aq-payment', N_('Payment'))
c8b173d6 Thomas Noël
support_substitution_variables = True
c43d0b77 Frédéric Péters
9d9d1ebd Thomas Noël
subject = None
c8b173d6 Thomas Noël
details = None
c43d0b77 Frédéric Péters
amount = None
regie_id = None
f0a875e0 Thomas Noël
next_status = None
700cf8e8 Benjamin Dauvergne
request_kwargs = {}
c43d0b77 Frédéric Péters
54a05fc5 Thomas NOEL
def is_available(self):
return is_payment_supported()
is_available = classmethod(is_available)

c43d0b77 Frédéric Péters
def render_as_line(self):
69360cf9 Thomas Noël
if self.regie_id:
return _('Payable to %s' % Regie.get(self.regie_id).label)
c43d0b77 Frédéric Péters
else:
return _('Payable (not completed)')

def get_parameters(self):
700cf8e8 Benjamin Dauvergne
return ('subject', 'details', 'amount', 'regie_id', 'next_status',
'request_kwargs')
c43d0b77 Frédéric Péters
4d7104fb Frédéric Péters
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
9d9d1ebd Thomas Noël
if 'subject' in parameters:
form.add(StringWidget, '%ssubject' % prefix, title=_('Subject'),
value=self.subject, size=40)
c8b173d6 Thomas Noël
if 'details' in parameters:
form.add(TextWidget, '%sdetails' % prefix, title=_('Details'),
value=self.details, cols=80, rows=10)
c43d0b77 Frédéric Péters
if 'amount' in parameters:
form.add(StringWidget, '%samount' % prefix, title=_('Amount'), value=self.amount)
if 'regie_id' in parameters:
form.add(SingleSelectWidget, '%sregie_id' % prefix,
title=_('Regie'), value=self.regie_id,
options = [(None, '---')] + [(x.id, x.label) for x in Regie.select()])
f0a875e0 Thomas Noël
if 'next_status' in parameters:
form.add(SingleSelectWidget, '%snext_status' % prefix,
title=_('Status after validation'), value = self.next_status,
hint=_('Used only if the current status of the form does not contain any "Payment Validation" item'),
options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])
700cf8e8 Benjamin Dauvergne
if 'request_kwargs' in parameters:
keys = ['name', 'email', 'address', 'phone', 'info1', 'info2', 'info3']
hint = ''
hint +=_('If the value starts by = it will be '
'interpreted as a Python expression.')
hint += ' '
hint += _('Standard keys are: %s.') % (', '.join(keys))
form.add(WidgetDict, 'request_kwargs',
title=_('Parameters for the payment system'),
hint=hint,
value = self.request_kwargs)
c43d0b77 Frédéric Péters
def perform(self, formdata):
3e1fe4cf Thomas Noël
invoice = Invoice(regie_id=self.regie_id, formdef_id=formdata.formdef.id)
c6ba8333 Thomas Noël
invoice.user_id = formdata.user_id
invoice.user_hash = formdata.user_hash
c43d0b77 Frédéric Péters
invoice.formdata_id = formdata.id
f0a875e0 Thomas Noël
invoice.next_status = self.next_status
9d9d1ebd Thomas Noël
if self.subject:
invoice.subject = template_on_formdata(formdata, self.compute(self.subject))
c8b173d6 Thomas Noël
else:
9d9d1ebd Thomas Noël
invoice.subject = _('%(form_name)s #%(formdata_id)s') % {
c8b173d6 Thomas Noël
'form_name': formdata.formdef.name,
'formdata_id': formdata.id }
invoice.details = template_on_formdata(formdata, self.compute(self.details))
403b9210 Benjamin Dauvergne
invoice.amount = Decimal(self.compute(self.amount))
391939ac Benjamin Dauvergne
invoice.date = dt.now()
700cf8e8 Benjamin Dauvergne
invoice.request_kwargs = {}
90c63943 Benjamin Dauvergne
if self.request_kwargs:
for key, value in self.request_kwargs.iteritems():
invoice.request_kwargs[key] = self.compute(value)
c43d0b77 Frédéric Péters
invoice.store()
c6ba8333 Thomas Noël
# add a message in formdata.evolution
evo = Evolution()
evo.time = time.localtime()
evo.status = formdata.status
evo.add_part(InvoiceEvolutionPart('create', invoice))
if not formdata.evolution:
formdata.evolution = []
formdata.evolution.append(evo)
formdata.store()
# redirect the user to "my invoices"
3e1fe4cf Thomas Noël
return get_publisher().get_frontoffice_url() + '/myspace/invoices/'
c43d0b77 Frédéric Péters
register_item_class(PaymentWorkflowStatusItem)

c6ba8333 Thomas Noël
class PaymentCancelWorkflowStatusItem(WorkflowStatusItem):
description = N_('Payment Cancel')
key = 'payment-cancel'
endpoint = False
315a95c4 Frédéric Péters
category = ('aq-payment', N_('Payment'))
c6ba8333 Thomas Noël
reason = None
regie_id = None

54a05fc5 Thomas NOEL
def is_available(self):
return is_payment_supported()
is_available = classmethod(is_available)

c6ba8333 Thomas Noël
def render_as_line(self):
if self.regie_id:
if self.regie_id == '_all':
return _('Cancel all Payments')
else:
return _('Cancel Payments for %s' % Regie.get(self.regie_id).label)
else:
return _('Cancel Payments (non completed)')

def get_parameters(self):
return ('reason', 'regie_id')

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

def perform(self, formdata):
invoices_id = []
# get all invoices for the formdata and the selected regie
for evo in [evo for evo in formdata.evolution if evo.parts]:
for part in [part for part in evo.parts if isinstance(part, InvoiceEvolutionPart)]:
if part.action == 'create':
invoices_id.append(part.id)
elif part.id in invoices_id:
invoices_id.remove(part.id)
invoices = [Invoice.get(id) for id in invoices_id]
# select invoices for the selected regie (if not "all regies")
if self.regie_id != '_all':
invoices = [i for i in invoices if i.regie_id == self.regie_id]
# security filter: check user
invoices = [i for i in invoices if (i.user_id == formdata.user_id) \
or (i.user_hash == formdata.user_hash)]
# security filter: check formdata & formdef
invoices = [i for i in invoices if (i.formdata_id == formdata.id) \
and (i.formdef_id == formdata.formdef.id)]
evo = Evolution()
evo.time = time.localtime()
for invoice in invoices:
if not (invoice.paid or invoice.canceled):
invoice.cancel(self.reason)
evo.add_part(InvoiceEvolutionPart('cancel', invoice))
if not formdata.evolution:
formdata.evolution = []
formdata.evolution.append(evo)
formdata.store()
return get_publisher().get_frontoffice_url() + '/myspace/invoices/'

register_item_class(PaymentCancelWorkflowStatusItem)


5955b92a Benjamin Dauvergne
def request_payment(invoice_ids, url, add_regie=True):
3e1fe4cf Thomas Noël
for invoice_id in invoice_ids:
if not Invoice.check_crc(invoice_id):
raise KeyError()
391939ac Benjamin Dauvergne
invoices = [ Invoice.get(invoice_id) for invoice_id in invoice_ids ]
c6ba8333 Thomas Noël
invoices = [ i for i in invoices if not (i.paid or i.canceled) ]
391939ac Benjamin Dauvergne
regie_ids = set([invoice.regie_id for invoice in invoices])
c6ba8333 Thomas Noël
# Do not apply if more than one regie is used or no invoice is not paid or canceled
391939ac Benjamin Dauvergne
if len(invoices) == 0 or len(regie_ids) != 1:
3e1fe4cf Thomas Noël
url = get_publisher().get_frontoffice_url()
if get_session().user:
# FIXME: add error messages
url += '/myspace/invoices/'
return redirect(url)
5955b92a Benjamin Dauvergne
if add_regie:
url = '%s%s' % (url, list(regie_ids)[0])
391939ac Benjamin Dauvergne
transaction = Transaction()
transaction.store()
transaction.invoice_ids = invoice_ids
transaction.start = dt.now()

amount = Decimal(0)
for invoice in invoices:
amount += Decimal(invoice.amount)

regie = Regie.get(invoice.regie_id)
payment = regie.get_payment_object()
700cf8e8 Benjamin Dauvergne
# initialize request_kwargs using informations from the first invoice
# and update using current user informations
request_kwargs = getattr(invoices[0], 'request_kwargs', {})
request = get_request()
if request.user and request.user.email:
request_kwargs['email'] = request.user.email
if request.user and request.user.display_name:
request_kwargs['name'] = request.user.display_name
(order_id, kind, data) = payment.request(amount, next_url=url, **request_kwargs)
f0a875e0 Thomas Noël
transaction.order_id = order_id
391939ac Benjamin Dauvergne
transaction.store()

47698115 Benjamin Dauvergne
for invoice in invoices:
if invoice.formdef_id and invoice.formdata_id:
formdef = FormDef.get(invoice.formdef_id)
formdata = formdef.data_class().get(invoice.formdata_id)
evo = Evolution()
evo.time = time.localtime()
evo.status = formdata.status
evo.add_part(InvoiceEvolutionPart('try', invoice,
transaction=transaction))
if not formdata.evolution:
formdata.evolution = []
formdata.evolution.append(evo)
formdata.store()

391939ac Benjamin Dauvergne
if kind == eopayment.URL:
return redirect(data)
elif kind == eopayment.FORM:
raise NotImplementedError()
else:
raise NotImplementedError()

c43d0b77 Frédéric Péters
class PaymentValidationWorkflowStatusItem(WorkflowStatusItem):
description = N_('Payment Validation')
key = 'payment-validation'
endpoint = False
315a95c4 Frédéric Péters
category = ('aq-payment', N_('Payment'))
c43d0b77 Frédéric Péters
next_status = None

54a05fc5 Thomas NOEL
def is_available(self):
return is_payment_supported()
is_available = classmethod(is_available)

c43d0b77 Frédéric Péters
def render_as_line(self):
return _('Wait for payment validation')

def get_parameters(self):
return ('next_status',)

4d7104fb Frédéric Péters
def add_parameters_widgets(self, form, parameters, prefix='', formdef=None):
c43d0b77 Frédéric Péters
if 'next_status' in parameters:
form.add(SingleSelectWidget, '%snext_status' % prefix,
title=_('Status once validated'), value = self.next_status,
options = [(None, '---')] + [(x.id, x.name) for x in self.parent.parent.possible_status])

register_item_class(PaymentValidationWorkflowStatusItem)
cfb41dcc Frédéric Péters

3c0aeb99 Frédéric Péters
class PublicPaymentRegieBackDirectory(Directory):
46bdb4f7 Benjamin Dauvergne
def __init__(self, asynchronous):
self.asynchronous = asynchronous

3c0aeb99 Frédéric Péters
def _q_lookup(self, component):
391939ac Benjamin Dauvergne
logger = get_logger()
f1ab9b59 Benjamin Dauvergne
request = get_request()
query_string = get_request().get_query()
if request.get_method() == 'POST' and query_string == '':
query_string = urllib.urlencode(request.form)
3c0aeb99 Frédéric Péters
try:
regie = Regie.get(component)
except KeyError:
raise errors.TraversalError()
46bdb4f7 Benjamin Dauvergne
if self.asynchronous:
logger.debug('received asynchronous notification %r' % query_string)
3c0aeb99 Frédéric Péters
payment = regie.get_payment_object()
6caf12b6 Benjamin Dauvergne
payment_response = payment.response(query_string)
46bdb4f7 Benjamin Dauvergne
logger.debug('payment response %r', payment_response)
6caf12b6 Benjamin Dauvergne
order_id = payment_response.order_id
bank_data = payment_response.bank_data

b99fb0fa Benjamin Dauvergne
transaction = Transaction.get_on_index(order_id, 'order_id', ignore_errors=True)
if transaction is None:
raise errors.TraversalError()
commit = False
if not transaction.end:
commit = True
391939ac Benjamin Dauvergne
transaction.end = dt.now()
transaction.bank_data = bank_data
transaction.store()
b99fb0fa Benjamin Dauvergne
if payment_response.signed and payment_response.is_paid() and commit:
logger.info('transaction %s successful, bankd_id:%s bank_data:%s' % (
order_id, payment_response.transaction_id, bank_data))
391939ac Benjamin Dauvergne
for invoice_id in transaction.invoice_ids:
f0a875e0 Thomas Noël
# all invoices are now paid
391939ac Benjamin Dauvergne
invoice = Invoice.get(invoice_id)
90c63943 Benjamin Dauvergne
invoice.pay()
f0a875e0 Thomas Noël
# workflow for each related formdata
3e1fe4cf Thomas Noël
if invoice.formdef_id and invoice.formdata_id:
f0a875e0 Thomas Noël
next_status = invoice.next_status
3e1fe4cf Thomas Noël
formdef = FormDef.get(invoice.formdef_id)
formdata = formdef.data_class().get(invoice.formdata_id)
39e48d52 Thomas NOEL
wf_status = formdata.get_status()
3e1fe4cf Thomas Noël
for item in wf_status.items:
if isinstance(item, PaymentValidationWorkflowStatusItem):
f0a875e0 Thomas Noël
next_status = item.next_status
break
if next_status is not None:
formdata.status = 'wf-%s' % next_status
evo = Evolution()
evo.time = time.localtime()
evo.status = formdata.status
d0cbe280 Benjamin Dauvergne
evo.add_part(InvoiceEvolutionPart('pay', invoice,
transaction=transaction))
f0a875e0 Thomas Noël
if not formdata.evolution:
formdata.evolution = []
formdata.evolution.append(evo)
formdata.store()
# performs the items of the new status
formdata.perform_workflow()

b99fb0fa Benjamin Dauvergne
elif payment_response.is_error() and commit:
6caf12b6 Benjamin Dauvergne
logger.error('transaction %s finished with failure, bank_data:%s' % (
order_id, bank_data))
b99fb0fa Benjamin Dauvergne
elif commit:
46bdb4f7 Benjamin Dauvergne
logger.info('transaction %s is in intermediate state, bank_data:%s' % (
order_id, bank_data))
if payment_response.return_content != None and self.asynchronous:
6caf12b6 Benjamin Dauvergne
get_response().set_content_type('text/plain')
return payment_response.return_content
else:
46bdb4f7 Benjamin Dauvergne
if payment_response.is_error():
6caf12b6 Benjamin Dauvergne
# TODO: here return failure message
get_session().message = ('info', _('Payment failed'))
46bdb4f7 Benjamin Dauvergne
else:
# TODO: Here return success message
get_session().message = ('error', _('Payment succeeded'))
6caf12b6 Benjamin Dauvergne
url = get_publisher().get_frontoffice_url()
if get_session().user:
url += '/myspace/invoices/'
return redirect(url)
3c0aeb99 Frédéric Péters
cfb41dcc Frédéric Péters
class PublicPaymentDirectory(Directory):
46bdb4f7 Benjamin Dauvergne
_q_exports = ['', 'init', 'back', 'back_asynchronous']

back = PublicPaymentRegieBackDirectory(False)
back_asynchronous = PublicPaymentRegieBackDirectory(True)
cfb41dcc Frédéric Péters
3c0aeb99 Frédéric Péters
cfb41dcc Frédéric Péters
def init(self):
3e1fe4cf Thomas Noël
invoice_ids = get_request().form.get('invoice_ids').split(' ')

for invoice_id in invoice_ids:
if not Invoice.check_crc(invoice_id):
raise KeyError()

url = get_publisher().get_frontoffice_url() + '/payment/back/'
cfb41dcc Frédéric Péters
391939ac Benjamin Dauvergne
return request_payment(invoice_ids, url)
90c63943 Benjamin Dauvergne
def notify_new_invoice(invoice):
notify_invoice(invoice, 'payment-new-invoice-email')

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

def notify_canceled_invoice(invoice):
notify_invoice(invoice, 'payment-invoice-deleted-email')

def notify_invoice(invoice, template):
user = invoice.get_user()
assert user is not None
regie = Regie.get(id=invoice.regie_id)
emails.custom_ezt_email(template, {
'user': user,
'invoice': invoice,
'regie': regie,
'invoice_url': invoice.payment_url()
}, user.email, fire_and_forget = True)