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)
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
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)
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()
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)
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)
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)
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.")
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)
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
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
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
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()
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)
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'])
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")
def get_object(self, **kwargs): return get_invoicing_config_instance()
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))
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
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))
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))
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")