Example #1
0
 def test_api_pull_invoice_with_correct_data(self):
     """
     pull_invoice() called with correct data generates and sends the invoice to customer
     """
     invoicing_config = get_invoicing_config_instance()
     invoicing_config.pull_invoice = True
     invoicing_config.save(using=UMBRELLA)
     Balance.objects.using('wallets').create(
         service_id='54ad2bd9b37b335a18fe5801', mail_count=100)
     number = 'INV001'
     data = {
         'api_signature': 'top_secret_token',
         'invoice_number': number,
         'reference_id': 'Ext_ID_1',
         'amount': 5000,
         'due_date': '2035-01-01',
         'quantity': 1
     }
     response = self.client.post(reverse('billing:pull_invoice'), data=data)
     self.assertEqual(response.status_code, 200)
     resp = json.loads(response.content)
     self.assertTrue(resp['success'])
     self.assertEqual(Invoice.objects.filter(number=number).count(), 1)
     self.assertEqual(
         XEmailObject.objects.filter(to='*****@*****.**').count(), 1)
     self.assertEqual(
         Balance.objects.using('wallets').get(
             service_id='54ad2bd9b37b335a18fe5801').mail_count, 99)
Example #2
0
 def get_default_tolerance():
     try:
         from ikwen.billing.utils import get_invoicing_config_instance
         invoicing_config = get_invoicing_config_instance()
         return invoicing_config.tolerance
     except:
         return 1
Example #3
0
 def test_pay_invoice_with_invoicing_config_return_url(self):
     """
     If InvoicingConfig has a return_url, then successful payment hits that
     return_url with the following parameters:
         reference_id: Reference ID of the subscription
         invoice_number:
         amount_paid
         extra_months: Extra months the customer decided to in addition of those of the current invoice
     """
     invoice_id = '56eb6d04b379d531e01237d3'
     sub_id = '56eb6d04b37b3379c531e013'
     Subscription.objects.filter(pk=sub_id).update(
         expiry=datetime.now().date())
     invoicing_config = get_invoicing_config_instance()
     invoicing_config.return_url = 'http://localhost/notify/'
     invoicing_config.save(using=UMBRELLA)
     self.client.login(username='******', password='******')
     response = self.client.post(reverse('billing:momo_set_checkout'),
                                 {'product_id': invoice_id})
     self.assertEqual(response.status_code, 200)
     # Init payment from Checkout page
     response = self.client.get(reverse('billing:init_momo_transaction'),
                                data={'phone': '677003321'})
     json_resp = json.loads(response.content)
     tx_id = json_resp['tx_id']
     response = self.client.get(
         reverse('billing:check_momo_transaction_status'),
         data={'tx_id': tx_id})
     self.assertEqual(response.status_code, 200)
     json_resp = json.loads(response.content)
     self.assertTrue(json_resp['success'])
     self.assertEqual(
         Invoice.objects.get(pk=invoice_id).status, Invoice.PAID)
     expiry = (datetime.now() + timedelta(days=30)).date()
     self.assertEqual(Subscription.objects.get(pk=sub_id).expiry, expiry)
Example #4
0
def notify_payment(payment):
    invoice = payment.invoice
    member = invoice.member
    service = get_service_instance()
    config = service.config

    invoicing_config = get_invoicing_config_instance()
    try:
        invoice_pdf_file = generate_pdf_invoice(invoicing_config, invoice)
    except:
        invoice_pdf_file = None

    if member.email:
        invoice_url = service.url + reverse('billing:invoice_detail',
                                            args=(invoice.id, ))
        subject, message, sms_text = get_payment_confirmation_message(
            payment, member)
        html_content = get_mail_content(
            subject,
            message,
            template_name='billing/mails/notice.html',
            extra_context={
                'member_name': member.first_name,
                'invoice': invoice,
                'cta': _("View invoice"),
                'invoice_url': invoice_url
            })
        sender = '%s <no-reply@%s>' % (config.company_name, service.domain)
        msg = XEmailMessage(subject, html_content, sender, [member.email])
        if invoice_pdf_file:
            msg.attach_file(invoice_pdf_file)
        msg.content_subtype = "html"
        Thread(target=lambda m: m.send(), args=(msg, )).start()
Example #5
0
 def get(self, request, *args, **kwargs):
     action = request.GET.get('action')
     invoice_id = kwargs['invoice_id']
     try:
         invoice = Invoice.objects.select_related(
             'member', 'subscription').get(pk=invoice_id)
     except Invoice.DoesNotExist:
         raise Http404("Invoice not found")
     if action == 'cash_in':
         return self.cash_in(invoice, request)
     if action == 'generate_pdf':
         from ikwen.billing.utils import generate_pdf_invoice
         invoicing_config = get_invoicing_config_instance()
         invoice_pdf_file = generate_pdf_invoice(invoicing_config, invoice)
         media_root = getattr(settings, 'MEDIA_ROOT')
         media_url = getattr(settings, 'MEDIA_URL')
         return HttpResponseRedirect(
             invoice_pdf_file.replace(media_root, media_url))
     member = invoice.member
     if member and not member.is_ghost:
         if request.user.is_authenticated() and not request.user.is_staff:
             if request.user != member:
                 next_url = reverse('ikwen:sign_in')
                 next_url += '?next=' + reverse('billing:invoice_detail',
                                                args=(invoice.id, ))
                 return HttpResponseRedirect(next_url)
     return super(InvoiceDetail, self).get(request, *args, **kwargs)
Example #6
0
 def test_shutdown_customers_services(self):
     invoicing_config = get_invoicing_config_instance()
     due_date = datetime.now() - timedelta(days=invoicing_config.tolerance +
                                           3)
     Invoice.objects.all().update(due_date=due_date, status=Invoice.OVERDUE)
     suspend_customers_services()
     sent = Invoice.objects.filter(status=Invoice.EXCEEDED).count()
     self.assertEqual(sent, 4)
Example #7
0
 def test_send_invoices(self):
     Invoice.objects.all().delete()
     invoicing_config = get_invoicing_config_instance()
     expiry = datetime.now() + timedelta(days=invoicing_config.gap)
     Subscription.objects.all().update(expiry=expiry)
     send_invoices()
     sent = Invoice.objects.all().count()
     self.assertEqual(sent, 3)
Example #8
0
 def test_api_pull_invoice_with_invalid_signature(self):
     invoicing_config = get_invoicing_config_instance()
     invoicing_config.pull_invoice = True
     invoicing_config.save(using=UMBRELLA)
     data = {'api_signature': 'wrong_signature', 'invoice_number': 'INV001'}
     response = self.client.post(reverse('billing:pull_invoice'), data=data)
     self.assertEqual(response.status_code, 200)
     resp = json.loads(response.content)
     self.assertEqual(resp['error'], u"Invalide API Signature.")
Example #9
0
 def test_send_invoice_overdue_notices(self):
     invoicing_config = get_invoicing_config_instance()
     due_date = datetime.now() - timedelta(days=1)
     last_reminder = datetime.now() - timedelta(
         days=invoicing_config.reminder_delay)
     Invoice.objects.all().update(due_date=due_date,
                                  last_reminder=last_reminder)
     send_invoice_overdue_notices()
     sent = Invoice.objects.filter(overdue_notices_sent=1).count()
     self.assertEqual(sent, 3)
Example #10
0
 def get_context_data(self, **kwargs):
     context = super(ChangeSubscription, self).get_context_data(**kwargs)
     invoicing_config = get_invoicing_config_instance()
     context['default_tolerance'] = invoicing_config.tolerance
     obj = context['obj']
     product_costs = {}
     for product in Product._default_manager.filter(is_active=True):
         product_costs[str(product.id)] = product.cost
     context['product_costs'] = product_costs
     context['invoice_list'] = Invoice.objects.select_related(
         'subscription').filter(subscription=obj)[:10]
     return context
Example #11
0
 def get_context_data(self, **kwargs):
     context = super(ServiceDetail, self).get_context_data(**kwargs)
     invoicing_config = get_invoicing_config_instance(UMBRELLA)
     service_id = kwargs.get('service_id')
     if not service_id:
         service_id = getattr(settings, 'IKWEN_SERVICE_ID')
     srvce = Service.objects.using(UMBRELLA).get(pk=service_id)
     invoice = Invoice.get_last(srvce)
     now = datetime.now()
     if invoice:
         srvce.last_payment = invoice.created_on
     if not srvce.version or srvce.version == Service.FREE:
         srvce.expiry = None
     else:
         srvce.next_invoice_on = srvce.expiry - timedelta(
             days=invoicing_config.gap)
         if srvce.expiry < now.date():
             srvce.expired = True
         if now.date() > srvce.next_invoice_on:
             days = get_billing_cycle_days_count(srvce.billing_cycle)
             srvce.next_invoice_on = srvce.next_invoice_on + timedelta(
                 days=days)
         srvce.next_invoice_amount = srvce.monthly_cost * get_billing_cycle_months_count(
             srvce.billing_cycle)
         srvce.pending_invoice_count = Invoice.objects.filter(
             subscription=srvce, status=Invoice.PENDING).count()
     try:
         support_code = SupportCode.objects.using(UMBRELLA).filter(
             service=srvce).order_by('-id')[0]
     except IndexError:
         support_code = None
     if support_code and support_code.expiry < now:
         support_code.expired = True
     from echo.models import Balance
     echo_balance, update = Balance.objects.using('wallets').get_or_create(
         service_id=srvce.id)
     context[
         'srvce'] = srvce  # Service named srvce in context to avoid collision with service from template_context_processors
     context['support_code'] = support_code
     context['echo_balance'] = echo_balance
     context['billing_cycles'] = Service.BILLING_CYCLES_CHOICES
     return context
Example #12
0
def set_invoice_checkout(request, *args, **kwargs):
    """
    This function has no URL associated with it.
    It serves as ikwen setting "MOMO_BEFORE_CHECKOUT"
    """
    invoice_id = request.POST['product_id']
    invoice = Invoice.objects.select_related('subscription').get(pk=invoice_id)
    service = get_service_instance()
    config = service.config
    invoicing_config = get_invoicing_config_instance()
    try:
        extra_months = int(request.POST.get('extra_months', '0'))
    except ValueError:
        extra_months = 0
    try:
        aggr = Payment.objects.filter(invoice=invoice).aggregate(Sum('amount'))
        amount_paid = aggr['amount__sum']
    except:
        amount_paid = 0
    amount = invoice.amount - amount_paid
    if extra_months:
        amount += invoice.service.monthly_cost * extra_months
    if invoicing_config.processing_fees_on_customer:
        amount += config.ikwen_share_fixed
    if amount > MOMO_MAX_AMOUNT:
        amount = MOMO_MAX_AMOUNT
    if amount % 50 > 0:
        amount = (amount / 50 +
                  1) * 50  # Mobile Money Payment support only multiples of 50

    payment = Payment.objects.create(invoice=invoice,
                                     method=Payment.MOBILE_MONEY,
                                     amount=amount)
    notification_url = reverse('billing:confirm_service_invoice_payment',
                               args=(payment.id, extra_months))
    cancel_url = reverse('billing:invoice_detail', args=(invoice_id, ))
    return_url = reverse('billing:invoice_detail', args=(invoice_id, ))
    return payment, amount, notification_url, return_url, cancel_url
Example #13
0
    def save_formset(self, request, form, formset, change):
        instances = formset.save(commit=False)
        total_amount = 0
        instance = None
        for instance in instances:
            if isinstance(instance, Payment):
                total_amount += instance.amount
                if instance.cashier:
                    # Instances with non null cashier are those that previously existed.
                    # setting them to None allows to ignore them at the end of the loop
                    # since we want to undertake action only for newly added Payment
                    instance = None
                    continue
                instance.cashier = request.user
                instance.save()
        if instance:
            # TODO: Check if the last payment is newly added
            # Notice email is sent only for the last saved Payment,
            # this is why this code is not run within the "for" loop above
            member = instance.invoice.subscription.member
            service = get_service_instance()
            config = service.config
            invoice = instance.invoice
            s = invoice.service
            days = get_days_count(invoice.months_count)
            if s.status == Service.SUSPENDED:
                invoicing_config = get_invoicing_config_instance()
                days -= invoicing_config.tolerance  # Catch-up days that were offered before service suspension
                expiry = datetime.now() + timedelta(days=days)
                expiry = expiry.date()
            elif s.expiry:
                expiry = s.expiry + timedelta(days=days)
            else:
                expiry = datetime.now() + timedelta(days=days)
                expiry = expiry.date()
            s.expiry = expiry
            s.status = Service.ACTIVE
            if invoice.is_one_off:
                s.version = Service.FULL
            s.save()
            share_payment_and_set_stats(invoice, invoice.months_count)

            if s.retailer:
                db = s.retailer.database
                Service.objects.using(db).filter(pk=s.id).update(
                    expiry=s.expiry, status=s.status, version=s.version)

            sudo_group = Group.objects.using(UMBRELLA).get(name=SUDO)
            add_event(service,
                      PAYMENT_CONFIRMATION,
                      group_id=sudo_group.id,
                      object_id=invoice.id)
            add_event(service,
                      PAYMENT_CONFIRMATION,
                      member=member,
                      object_id=invoice.id)
            subject, message, sms_text = get_payment_confirmation_message(
                instance, member)
            if member.email:
                try:
                    currency = Currency.active.default().symbol
                except:
                    currency = config.currency_code
                invoice_url = service.url + reverse('billing:invoice_detail',
                                                    args=(invoice.id, ))
                subject, message, sms_text = get_payment_confirmation_message(
                    instance, member)
                html_content = get_mail_content(
                    subject,
                    message,
                    template_name='billing/mails/notice.html',
                    extra_context={
                        'member_name': member.first_name,
                        'invoice': invoice,
                        'cta': _("View invoice"),
                        'invoice_url': invoice_url,
                        'currency': currency
                    })
                sender = '%s <no-reply@%s>' % (config.company_name,
                                               service.domain)
                msg = EmailMessage(subject, html_content, sender,
                                   [member.email])
                msg.content_subtype = "html"
                Thread(target=lambda m: m.send(), args=(msg, )).start()
            if sms_text:
                if member.phone:
                    if config.sms_sending_method == Config.HTTP_API:
                        send_sms(member.phone, sms_text)
                    else:
                        QueuedSMS.objects.create(recipient=member.phone,
                                                 text=sms_text)
            if total_amount >= invoice.amount:
                invoice.status = AbstractInvoice.PAID
                invoice.save()
Example #14
0
def set_invoice_checkout(request, *args, **kwargs):
    """
    This function has no URL associated with it.
    It serves as ikwen setting "MOMO_BEFORE_CHECKOUT"
    """
    invoice_id = request.POST['product_id']
    invoice = Invoice.objects.select_related('subscription').get(pk=invoice_id)
    member = invoice.subscription.member
    if member and not member.is_ghost:
        if request.user != member:
            next_url = reverse('ikwen:sign_in')
            referrer = request.META.get('HTTP_REFERER')
            if referrer:
                next_url += '?' + urlquote(referrer)
            return HttpResponseRedirect(next_url)
    service = get_service_instance()
    config = service.config
    invoicing_config = get_invoicing_config_instance()
    try:
        extra_months = int(request.POST.get('extra_months', '0'))
    except ValueError:
        extra_months = 0
    try:
        aggr = Payment.objects.filter(invoice=invoice).aggregate(Sum('amount'))
        amount_paid = aggr['amount__sum']
    except IndexError:
        amount_paid = 0
    amount = invoice.amount - amount_paid
    if extra_months:
        amount += invoice.service.monthly_cost * extra_months
    if invoicing_config.processing_fees_on_customer:
        amount += config.ikwen_share_fixed

    signature = ''.join([random.SystemRandom().choice(string.ascii_letters + string.digits) for i in range(16)])
    mean = request.GET.get('mean', MTN_MOMO)
    request.session['mean'] = mean
    request.session['signature'] = signature
    request.session['amount'] = amount
    request.session['object_id'] = invoice_id
    request.session['extra_months'] = extra_months

    model_name = 'billing.Invoice'
    mean = request.GET.get('mean', MTN_MOMO)
    MoMoTransaction.objects.using(WALLETS_DB_ALIAS).filter(object_id=invoice_id).delete()
    tx = MoMoTransaction.objects.using(WALLETS_DB_ALIAS)\
        .create(service_id=service.id, type=MoMoTransaction.CASH_OUT, amount=amount, phone='N/A', model=model_name,
                object_id=invoice_id, task_id=signature, wallet=mean, username=request.user.username, is_running=True)
    notification_url = reverse('billing:confirm_service_invoice_payment', args=(tx.id, signature, extra_months))
    cancel_url = reverse('billing:invoice_detail', args=(invoice_id, ))
    return_url = reverse('billing:invoice_detail', args=(invoice_id, ))

    request.session['notif_url'] = service.url  # Orange Money only
    request.session['cancel_url'] = service.url + reverse('billing:invoice_detail', args=(invoice_id, )) # Orange Money only
    request.session['return_url'] = service.url + reverse('billing:invoice_detail', args=(invoice_id, ))

    if getattr(settings, 'UNIT_TESTING', False):
        return HttpResponse(json.dumps({'notification_url': notification_url}), content_type='text/json')
    gateway_url = getattr(settings, 'IKWEN_PAYMENT_GATEWAY_URL', 'http://payment.ikwen.com/v1')
    endpoint = gateway_url + '/request_payment'
    params = {
        'username': getattr(settings, 'IKWEN_PAYMENT_GATEWAY_USERNAME', service.project_name_slug),
        'amount': amount,
        'merchant_name': config.company_name,
        'notification_url': service.url + notification_url,
        'return_url': service.url + return_url,
        'cancel_url': service.url + cancel_url,
        'user_id': request.user.username
    }
    try:
        r = requests.get(endpoint, params)
        resp = r.json()
        token = resp.get('token')
        if token:
            next_url = gateway_url + '/checkoutnow/' + resp['token'] + '?mean=' + mean
        else:
            logger.error("%s - Init payment flow failed with URL %s and message %s" % (service.project_name, r.url, resp['errors']))
            messages.error(request, resp['errors'])
            next_url = cancel_url
    except:
        logger.error("%s - Init payment flow failed with URL." % service.project_name, exc_info=True)
        next_url = cancel_url
    return HttpResponseRedirect(next_url)
Example #15
0
def confirm_invoice_payment(request, *args, **kwargs):
    """
    This function has no URL associated with it.
    It serves as ikwen setting "MOMO_AFTER_CHECKOUT"
    """
    from echo.models import Balance
    from echo.utils import LOW_MAIL_LIMIT, notify_for_low_messaging_credit, notify_for_empty_messaging_credit
    tx = kwargs.get('transaction')
    now = datetime.now()
    service = get_service_instance()
    config = service.config
    invoicing_config = get_invoicing_config_instance()
    invoice_id = request.session['object_id']
    amount = request.session['amount']
    invoice = Invoice.objects.select_related('subscription').get(pk=invoice_id)
    invoice.paid += amount
    invoice.status = Invoice.PAID
    if invoicing_config.processing_fees_on_customer:
        invoice.processing_fees = config.ikwen_share_fixed
    invoice.save()
    payment = Payment.objects.create(invoice=invoice, method=Payment.MOBILE_MONEY,
                                     amount=amount, processor_tx_id=tx.processor_tx_id)
    subscription = invoice.subscription
    if invoicing_config.separate_billing_cycle:
        extra_months = request.session['extra_months']
        total_months = invoice.months_count + extra_months
        days = get_days_count(total_months)
    else:
        extra_months = 0
        days = invoice.subscription.product.duration
        total_months = None
    if subscription.status == Service.SUSPENDED:
        invoicing_config = get_invoicing_config_instance()
        days -= invoicing_config.tolerance  # Catch-up days that were offered before service suspension
        expiry = now + timedelta(days=days)
        expiry = expiry.date()
    elif subscription.expiry:
        expiry = subscription.expiry + timedelta(days=days)
    else:
        expiry = now + timedelta(days=days)
        expiry = expiry.date()
    subscription.expiry = expiry
    subscription.status = Service.ACTIVE
    subscription.save()
    mean = request.session['mean']
    share_payment_and_set_stats(invoice, total_months, mean)
    member = request.user
    sudo_group = Group.objects.using(UMBRELLA).get(name=SUDO)
    add_event(service, PAYMENT_CONFIRMATION, member=member, object_id=invoice.id)
    add_event(service, PAYMENT_CONFIRMATION, group_id=sudo_group.id, object_id=invoice.id)

    if invoicing_config.return_url:
        params = {'reference_id': subscription.reference_id, 'invoice_number': invoice.number,
                  'amount_paid': amount, 'processor_tx_id': tx.processor_tx_id, 'extra_months': extra_months}
        Thread(target=notify_event, args=(service, invoicing_config.return_url, params)).start()

    try:
        invoice_pdf_file = generate_pdf_invoice(invoicing_config, invoice)
    except:
        invoice_pdf_file = None

    balance, update = Balance.objects.using(WALLETS_DB_ALIAS).get_or_create(service_id=service.id)
    if member.email:
        if 0 < balance.mail_count < LOW_MAIL_LIMIT:
            notify_for_low_messaging_credit(service, balance)
        if balance.mail_count <= 0:
            notify_for_empty_messaging_credit(service, balance)
        else:
            try:
                currency = Currency.active.default().symbol
            except:
                currency = config.currency_code
            invoice_url = service.url + reverse('billing:invoice_detail', args=(invoice.id,))
            subject, message, sms_text = get_payment_confirmation_message(payment, member)
            html_content = get_mail_content(subject, message, template_name='billing/mails/notice.html',
                                            extra_context={'member_name': member.first_name, 'invoice': invoice,
                                                           'cta': _("View invoice"), 'invoice_url': invoice_url,
                                                           'currency': currency})
            sender = '%s <no-reply@%s>' % (config.company_name, service.domain)
            msg = XEmailMessage(subject, html_content, sender, [member.email])
            msg.content_subtype = "html"
            if invoice_pdf_file:
                msg.attach_file(invoice_pdf_file)
            balance.mail_count -= 1
            balance.save()
            Thread(target=lambda m: m.send(), args=(msg,)).start()
    return HttpResponseRedirect(request.session['return_url'])
Example #16
0
def confirm_service_invoice_payment(request, *args, **kwargs):
    """
    This view is run after successful user cashout "MOMO_AFTER_CHECKOUT"
    """
    status = request.GET['status']
    message = request.GET['message']
    operator_tx_id = request.GET['operator_tx_id']
    phone = request.GET['phone']
    tx_id = kwargs['tx_id']
    extra_months = int(kwargs['extra_months'])
    try:
        tx = MoMoTransaction.objects.using(WALLETS_DB_ALIAS).get(pk=tx_id, is_running=True)
        if not getattr(settings, 'DEBUG', False):
            tx_timeout = getattr(settings, 'IKWEN_PAYMENT_GATEWAY_TIMEOUT', 15) * 60
            expiry = tx.created_on + timedelta(seconds=tx_timeout)
            if datetime.now() > expiry:
                return HttpResponse("Transaction %s timed out." % tx_id)

        tx.status = status
        tx.message = 'OK' if status == MoMoTransaction.SUCCESS else message
        tx.processor_tx_id = operator_tx_id
        tx.phone = phone
        tx.is_running = False
        tx.save()
    except:
        raise Http404("Transaction %s not found" % tx_id)
    if status != MoMoTransaction.SUCCESS:
        return HttpResponse("Notification for transaction %s received with status %s" % (tx_id, status))
    invoice_id = tx.object_id
    amount = tx.amount
    signature = tx.task_id

    callback_signature = kwargs.get('signature')
    no_check_signature = request.GET.get('ncs')
    if getattr(settings, 'DEBUG', False):
        if not no_check_signature:
            if callback_signature != signature:
                return HttpResponse('Invalid transaction signature')
    else:
        if callback_signature != signature:
            return HttpResponse('Invalid transaction signature')
    now = datetime.now()
    ikwen_service = get_service_instance()
    invoice = Invoice.objects.get(pk=invoice_id)
    invoice.paid += amount
    invoice.status = Invoice.PAID
    invoice.save()
    payment = Payment.objects.create(invoice=invoice, method=Payment.MOBILE_MONEY,
                                     amount=amount, processor_tx_id=tx.processor_tx_id)
    service = invoice.service
    total_months = invoice.months_count + extra_months
    days = get_days_count(total_months)
    invoicing_config = get_invoicing_config_instance()
    if service.status == Service.SUSPENDED:
        days -= invoicing_config.tolerance  # Catch-up days that were offered before service suspension
        expiry = now + timedelta(days=days)
        expiry = expiry.date()
    elif service.expiry:
        expiry = service.expiry + timedelta(days=days)
    else:
        expiry = now + timedelta(days=days)
        expiry = expiry.date()
    service.expiry = expiry
    service.status = Service.ACTIVE
    if invoice.is_one_off:
        service.version = Service.FULL
        try:
            support_bundle = SupportBundle.objects.get(type=SupportBundle.TECHNICAL, channel=SupportBundle.PHONE, cost=0)
            token = ''.join([random.SystemRandom().choice(string.digits) for i in range(6)])
            support_expiry = now + timedelta(days=support_bundle.duration)
            SupportCode.objects.create(service=service, token=token, bundle=support_bundle,
                                       balance=support_bundle.quantity, expiry=support_expiry)
            logger.debug("Free Support Code created for %s" % service)
        except SupportBundle.DoesNotExist:
            logger.error("Free Support Code not created for %s" % service, exc_info=True)
    service.save()
    mean = tx.wallet
    is_early_payment = False
    if service.app.slug == 'kakocase' or service.app.slug == 'webnode':
        if invoice.due_date <= now.date():
            is_early_payment = True
        refill_tsunami_messaging_bundle(service, is_early_payment)
    share_payment_and_set_stats(invoice, total_months, mean)
    member = service.member
    vendor = service.retailer
    vendor_is_dara = vendor and vendor.app.slug == DARAJA
    if vendor and not vendor_is_dara:
        add_database_to_settings(vendor.database)
        sudo_group = Group.objects.using(vendor.database).get(name=SUDO)
    else:
        vendor = ikwen_service
        sudo_group = Group.objects.using(UMBRELLA).get(name=SUDO)
    add_event(vendor, PAYMENT_CONFIRMATION, member=member, object_id=invoice.id)
    add_event(vendor, PAYMENT_CONFIRMATION, group_id=sudo_group.id, object_id=invoice.id)

    try:
        invoice_pdf_file = generate_pdf_invoice(invoicing_config, invoice)
    except:
        invoice_pdf_file = None

    if member.email:
        activate(member.language)
        invoice_url = service.url + reverse('billing:invoice_detail', args=(invoice.id,))
        subject, message, sms_text = get_payment_confirmation_message(payment, member)
        html_content = get_mail_content(subject, message, service=vendor, template_name='billing/mails/notice.html',
                                        extra_context={'member_name': member.first_name, 'invoice': invoice,
                                                       'cta': _("View invoice"), 'invoice_url': invoice_url,
                                                       'early_payment': is_early_payment})
        sender = '%s <no-reply@%s>' % (vendor.config.company_name, ikwen_service.domain)
        msg = XEmailMessage(subject, html_content, sender, [member.email])
        if vendor != ikwen_service and not vendor_is_dara:
            msg.service = vendor
        if invoice_pdf_file:
            msg.attach_file(invoice_pdf_file)
        msg.content_subtype = "html"
        if getattr(settings, 'UNIT_TESTING', False):
            msg.send()
        else:
            Thread(target=lambda m: m.send(), args=(msg,)).start()
    return HttpResponse("Notification received")
Example #17
0
 def get_object(self, **kwargs):
     return get_invoicing_config_instance()
Example #18
0
    def cash_in(self, invoice, request):
        if not request.user.has_perm('billing.ik_cash_in'):
            return HttpResponse(
                json.dumps({'error': "You're not allowed here"}))
        service = get_service_instance()
        config = service.config
        invoicing_config = get_invoicing_config_instance()
        if invoice.status == Invoice.PAID:
            return HttpResponse(json.dumps({'error': "Invoice already paid"}))
        amount = request.GET.get('amount', invoice.amount)
        try:
            amount = float(amount)
            if amount <= 0:
                raise ValueError()
        except ValueError:
            return HttpResponse(json.dumps({'error': "Invalid amount"}))
        member = invoice.member
        payment = Payment.objects.create(invoice=invoice,
                                         amount=amount,
                                         method=Payment.CASH,
                                         cashier=request.user)
        response = {'success': True, 'payment': payment.to_dict()}
        try:
            aggr = Payment.objects.filter(invoice=invoice).aggregate(
                Sum('amount'))
            amount_paid = aggr['amount__sum']
        except IndexError:
            amount_paid = 0

        if invoicing_config.return_url:
            try:
                subscription = invoice.subscription
                extra_months = request.GET.get('extra_months', 0)
                params = {
                    'reference_id': subscription.reference_id,
                    'invoice_number': invoice.number,
                    'amount_paid': amount,
                    'extra_months': extra_months
                }
                Thread(target=notify_event,
                       args=(service, invoicing_config.return_url,
                             params)).start()
            except:
                notice = "%s: Could not notify endpoint %s after cash in" % (
                    service, invoicing_config.return_url)
                logger.error(notice, exc_info=True)

        total = amount + amount_paid
        invoice.paid += amount
        if total >= invoice.amount:
            invoice.status = Invoice.PAID
            try:
                subscription = invoice.subscription
                subscription.status = Subscription.ACTIVE
                days = get_days_count(invoice.months_count)
                subscription.expiry += timedelta(days=days)
                subscription.save()
            except AttributeError:
                pass
        invoice.save()
        set_counters(service)
        increment_history_field(service, 'turnover_history', amount)
        increment_history_field(service, 'earnings_history', amount)
        increment_history_field(service, 'transaction_earnings_history',
                                amount)
        increment_history_field(service, 'invoice_earnings_history', amount)
        increment_history_field(service, 'transaction_count_history')
        increment_history_field(service, 'invoice_count_history')
        if member.email:
            from echo.models import Balance
            from echo.utils import notify_for_low_messaging_credit, LOW_MAIL_LIMIT, notify_for_empty_messaging_credit
            balance = Balance.objects.using(WALLETS_DB_ALIAS).get(
                service_id=service.id)
            if 0 < balance.mail_count < LOW_MAIL_LIMIT:
                notify_for_low_messaging_credit(service, balance)
            if balance.mail_count <= 0 and not getattr(settings,
                                                       'UNIT_TESTING', False):
                notify_for_empty_messaging_credit(service, balance)
                return HttpResponse(json.dumps(response))
            subject, message, sms = get_payment_confirmation_message(
                payment, member)
            html_content = get_mail_content(
                subject, message, template_name='billing/mails/notice.html')
            sender = '%s <no-reply@%s>' % (config.company_name, service.domain)
            msg = XEmailMessage(subject, html_content, sender, [member.email])
            msg.content_subtype = "html"
            try:
                with transaction.atomic(using='wallets'):
                    balance.mail_count -= 1
                    balance.save()
                    Thread(target=lambda m: m.send(), args=(msg, )).start()
            except:
                pass
        return HttpResponse(json.dumps(response))
Example #19
0
    def get_context_data(self, **kwargs):
        context = super(InvoiceDetail, self).get_context_data(**kwargs)
        invoice_id = self.kwargs['invoice_id']
        try:
            invoice = Invoice.objects.select_related(
                'subscription', 'member').get(pk=invoice_id)
        except Invoice.DoesNotExist:
            raise Http404("Invoice not found")
        try:
            subscription = invoice.subscription
            context['monthly_cost'] = subscription.monthly_cost
        except AttributeError:
            subscription = None
        try:
            product = subscription.product
        except AttributeError:
            product = None
        member = self.request.user
        if member.is_authenticated():
            if not invoice.member and not member.is_staff:
                invoice.member = member
                invoice.save()
            if subscription:
                mbr = subscription.member
                if not mbr or mbr.is_ghost:
                    if not member.is_staff:
                        subscription.member = member
                        subscription.save()
        context['invoice'] = invoice
        if not invoice.entries:
            if product and product.short_description:
                details = product.short_description
            elif subscription.details:
                details = subscription.details
            else:
                details = '------'
            context['details'] = details
        context['payment_mean_list'] = list(
            PaymentMean.objects.filter(is_active=True).order_by('-is_main'))
        context['invoicing_config'] = get_invoicing_config_instance()
        weblet = get_service_instance()
        if getattr(settings, 'IS_IKWEN', False):
            try:
                invoice_service = invoice.service
                retailer = invoice_service.retailer
                if retailer:
                    weblet = retailer
                context['customer_config'] = invoice_service.config
            except:
                pass
        context['vendor'] = weblet.config
        filename = '%s_Invoice_%s_%s.pdf' % (
            weblet.project_name_slug.upper(), invoice.number,
            invoice.date_issued.strftime("%Y-%m-%d"))
        media_root = getattr(settings, 'MEDIA_ROOT')
        if os.path.exists(media_root + filename):
            context['pdf_filename'] = filename

        # User may want to extend the payment above the default duration
        # Below are a list of possible extension dates on a year
        # TODO: Manage extensions for case where Product is bound to a duration (billing cycle)
        if not invoice.is_one_off:
            expiry = invoice.subscription.expiry
            exp_year = expiry.year
            exp_month = expiry.month
            exp_day = expiry.day
            extensions = []
            for i in (1, 2, 3, 6, 12):
                year = exp_year
                month = exp_month + i
                day = exp_day
                if month > 12:
                    year += 1
                    month = (exp_month + i) % 12
                    if month == 0:
                        month = 12
                valid_date = False
                while not valid_date:
                    try:
                        next_expiry = date(year, month, day)
                        extensions.append({'expiry': next_expiry, 'months': i})
                        valid_date = True
                    except:
                        day -= 1
            context['extensions'] = extensions
        return context
Example #20
0
def confirm_invoice_payment(request, *args, **kwargs):
    tx = kwargs[
        'tx']  # Decoration with @momo_gateway_callback makes 'tx' available in kwargs
    school = Service.objects.get(pk=tx.service_id)
    school_config = SchoolConfig.objects.get(service=school)
    ikwen_charges = tx.amount * school_config.ikwen_share_rate / 100

    tx.fees = ikwen_charges
    tx.save()
    mean = tx.wallet
    db = school.database
    add_database(db)
    invoice = Invoice.objects.using(db).get(pk=tx.object_id)
    payment = Payment.objects.using(db).create(
        invoice=invoice,
        method=Payment.MOBILE_MONEY,
        amount=tx.amount,
        processor_tx_id=tx.processor_tx_id)
    invoice.paid = tx.amount
    invoice.status = Invoice.PAID
    invoice.save()
    student = invoice.student
    if not school_config.is_public or (school_config.is_public
                                       and not invoice.is_tuition):
        amount = tx.amount - ikwen_charges
        school.raise_balance(amount, provider=mean)
    else:
        amount = tx.amount
    student.set_has_new(using=school.database)
    student.save(using='default')

    set_counters(school)
    increment_history_field(school, 'turnover_history', tx.amount)
    increment_history_field(school, 'earnings_history', amount)
    increment_history_field(school, 'transaction_count_history')
    increment_history_field(school, 'transaction_earnings_history', amount)

    member = invoice.member
    if member.email:
        try:
            currency = Currency.active.default().symbol
        except:
            currency = school_config.currency_code
        invoice_url = school.url + reverse('billing:invoice_detail',
                                           args=(invoice.id, ))
        subject, message, sms_text = get_payment_confirmation_message(
            payment, member)
        html_content = get_mail_content(
            subject,
            message,
            template_name='billing/mails/notice.html',
            extra_context={
                'member_name': member.first_name,
                'invoice': invoice,
                'cta': _("View invoice"),
                'invoice_url': invoice_url,
                'currency': currency
            })
        sender = '%s <no-reply@%s>' % (school_config.company_name,
                                       school.domain)
        msg = XEmailMessage(subject, html_content, sender, [member.email])
        msg.content_subtype = "html"
        bcc = [
            email.strip()
            for email in school_config.notification_emails.split(',')
            if email.strip()
        ]
        bcc += [school_config.contact_email, school.member.email]
        msg.bcc = list(set(bcc))
        try:
            invoicing_config = get_invoicing_config_instance()
            invoice_pdf_file = generate_pdf_invoice(invoicing_config, invoice)
            msg.attach_file(invoice_pdf_file)
        except:
            pass
    return HttpResponse(
        "Notification for transaction %s received with status %s" %
        (tx.id, tx.status))
Example #21
0
def confirm_my_kids_payment(request, *args, **kwargs):
    tx = kwargs['tx']
    invoice = Invoice.objects.get(pk=tx.object_id)
    school = invoice.school
    school_config = SchoolConfig.objects.get(service=school)
    ikwen_charges = tx.amount * school_config.my_kids_share_rate / 100
    tx.fees = ikwen_charges
    tx.save(using='wallets')
    mean = tx.wallet

    amount = tx.amount - ikwen_charges
    school.raise_balance(amount, provider=mean)
    share_payment_and_set_stats(invoice, mean, tx)

    invoice.paid = invoice.amount
    invoice.status = Invoice.PAID
    invoice.save()

    member = invoice.member
    student = invoice.student
    max_expiry = datetime(day=31, month=8, year=get_school_year() + 1)
    days = get_billing_cycle_days_count(invoice.my_kids_cycle)
    expiry = datetime.now() + timedelta(days=days)
    expiry = min(expiry, max_expiry)
    student.my_kids_expiry = expiry
    student.my_kids_expired = False
    student.save()

    db = school.database
    add_database(db)
    invoice.save(using=db)
    payment = Payment(invoice=invoice,
                      method=Payment.MOBILE_MONEY,
                      amount=tx.amount,
                      processor_tx_id=tx.processor_tx_id)
    if school_config.my_kids_share_rate < 100:
        # Payment appears in school log panel only if the have something to collect out of that
        payment.save(using=db)

    if member.email:
        try:
            currency = Currency.active.default().symbol
        except:
            currency = school_config.currency_code
        invoice_url = school.url + reverse('billing:invoice_detail',
                                           args=(invoice.id, ))
        subject, message, sms_text = get_payment_confirmation_message(
            payment, member)
        html_content = get_mail_content(
            subject,
            message,
            template_name='billing/mails/notice.html',
            extra_context={
                'member_name': member.first_name,
                'invoice': invoice,
                'cta': _("View invoice"),
                'invoice_url': invoice_url,
                'currency': currency
            })
        sender = '%s <no-reply@%s>' % (school_config.company_name,
                                       school.domain)
        msg = XEmailMessage(subject, html_content, sender, [member.email])
        msg.content_subtype = "html"
        try:
            invoicing_config = get_invoicing_config_instance()
            invoice_pdf_file = generate_pdf_invoice(invoicing_config, invoice)
            msg.attach_file(invoice_pdf_file)
        except:
            pass
    return HttpResponse(
        "Notification for transaction %s received with status %s" %
        (tx.id, tx.status))
Example #22
0
def confirm_service_invoice_payment(request, *args, **kwargs):
    """
    This view is run after successful user cashout "MOMO_AFTER_CHECKOUT"
    """
    tx = kwargs[
        'tx']  # Decoration with @momo_gateway_callback makes 'tx' available in kwargs
    extra_months = int(kwargs['extra_months'])
    payment = Payment.objects.select_related().get(pk=tx.object_id)
    payment.processor_tx_id = tx.processor_tx_id
    payment.save()
    invoice = payment.invoice
    now = datetime.now()
    ikwen_service = get_service_instance()
    invoice.paid += tx.amount
    if invoice.paid >= invoice.amount:
        invoice.status = Invoice.PAID
    invoice.save()
    service = invoice.service
    total_months = invoice.months_count + extra_months
    days = get_days_count(total_months)
    invoicing_config = get_invoicing_config_instance()
    if service.status == Service.SUSPENDED:
        days -= invoicing_config.tolerance  # Catch-up days that were offered before service suspension
        expiry = now + timedelta(days=days)
        expiry = expiry.date()
    elif service.expiry:
        expiry = service.expiry + timedelta(days=days)
    else:
        expiry = now + timedelta(days=days)
        expiry = expiry.date()
    service.expiry = expiry
    service.status = Service.ACTIVE
    if invoice.is_one_off:
        service.version = Service.FULL
        try:
            support_bundle = SupportBundle.objects.get(
                type=SupportBundle.TECHNICAL,
                channel=SupportBundle.PHONE,
                cost=0)
            token = ''.join([
                random.SystemRandom().choice(string.digits) for i in range(6)
            ])
            support_expiry = now + timedelta(days=support_bundle.duration)
            SupportCode.objects.create(service=service,
                                       token=token,
                                       bundle=support_bundle,
                                       balance=support_bundle.quantity,
                                       expiry=support_expiry)
            logger.debug("Free Support Code created for %s" % service)
        except SupportBundle.DoesNotExist:
            logger.error("Free Support Code not created for %s" % service,
                         exc_info=True)
    service.save()
    mean = tx.wallet
    is_early_payment = False
    if service.app.slug == 'kakocase' or service.app.slug == 'webnode':
        if invoice.due_date <= now.date():
            is_early_payment = True
        refill_tsunami_messaging_bundle(service, is_early_payment)
    share_payment_and_set_stats(invoice, total_months, mean, tx)
    member = service.member
    vendor = service.retailer
    vendor_is_dara = vendor and vendor.app.slug == DARAJA
    if vendor and not vendor_is_dara:
        add_database_to_settings(vendor.database)
        sudo_group = Group.objects.using(vendor.database).get(name=SUDO)
    else:
        vendor = ikwen_service
        sudo_group = Group.objects.using(UMBRELLA).get(name=SUDO)
    add_event(vendor,
              PAYMENT_CONFIRMATION,
              member=member,
              object_id=invoice.id)
    add_event(vendor,
              PAYMENT_CONFIRMATION,
              group_id=sudo_group.id,
              object_id=invoice.id)

    try:
        invoice_pdf_file = generate_pdf_invoice(invoicing_config, invoice)
    except:
        invoice_pdf_file = None

    if member.email:
        activate(member.language)
        invoice_url = ikwen_service.url + reverse('billing:invoice_detail',
                                                  args=(invoice.id, ))
        subject, message, sms_text = get_payment_confirmation_message(
            payment, member)
        html_content = get_mail_content(
            subject,
            message,
            service=vendor,
            template_name='billing/mails/notice.html',
            extra_context={
                'member_name': member.first_name,
                'invoice': invoice,
                'cta': _("View invoice"),
                'invoice_url': invoice_url,
                'early_payment': is_early_payment
            })
        sender = '%s <no-reply@%s>' % (vendor.config.company_name,
                                       ikwen_service.domain)
        msg = XEmailMessage(subject, html_content, sender, [member.email])
        if vendor != ikwen_service and not vendor_is_dara:
            msg.service = vendor
        if invoice_pdf_file:
            msg.attach_file(invoice_pdf_file)
        msg.content_subtype = "html"
        if getattr(settings, 'UNIT_TESTING', False):
            msg.send()
        else:
            Thread(target=lambda m: m.send(), args=(msg, )).start()
    return HttpResponse("Notification received")