Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Event Invoice Refactor #7279

Merged
merged 7 commits into from
Sep 24, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ WORKDIR /install
RUN apk update && \
apk add --virtual build-deps git gcc python3-dev musl-dev jpeg-dev zlib-dev libevent-dev file-dev libffi-dev openssl && \
apk add postgresql-dev
# PDF Generation: weasyprint (libffi-dev jpeg-dev already included above)
RUN apk add --virtual gdk-pixbuf-dev

ADD requirements.txt /requirements.txt
ADD requirements /requirements/
Expand All @@ -21,6 +23,9 @@ FROM base

COPY --from=builder /install /usr/local
RUN apk --no-cache add postgresql-libs ca-certificates libxslt jpeg zlib file libxml2
# PDF Generation: weasyprint
RUN apk --no-cache add cairo-dev pango-dev ttf-opensans
RUN fc-cache -f && rm -rf /var/cache/*

WORKDIR /data/app
ADD . .
Expand Down
2 changes: 1 addition & 1 deletion app/api/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from flask_rest_jsonapi.exceptions import ObjectNotFound
from marshmallow_jsonapi import fields
from marshmallow_jsonapi.flask import Schema
from sqlalchemy import or_, and_
from sqlalchemy import and_, or_
from sqlalchemy.orm.exc import NoResultFound

from app.api.bootstrap import api
Expand Down
11 changes: 8 additions & 3 deletions app/api/helpers/db.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import binascii
import logging
import os
import uuid

from flask import request
Expand Down Expand Up @@ -144,10 +146,13 @@ def get_new_slug(model, name):
return '{}-{}'.format(slug, uuid.uuid4().hex)


def get_new_identifier(model):
identifier = str(uuid.uuid4())
def get_new_identifier(model, length=None):
if not length:
identifier = str(uuid.uuid4())
else:
identifier = str(binascii.b2a_hex(os.urandom(int(length / 2))), 'utf-8')
count = get_count(model.query.filter_by(identifier=identifier))
if count == 0:
if not identifier.isdigit() and count == 0:
return identifier
else:
return get_new_identifier(model)
15 changes: 11 additions & 4 deletions app/api/helpers/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from flask import current_app
from PIL import Image
from sqlalchemy.orm.exc import NoResultFound
from weasyprint import HTML
from xhtml2pdf import pisa

from app.api.helpers.storage import UPLOAD_PATHS, UploadedFile, generate_hash, upload
Expand Down Expand Up @@ -351,6 +352,8 @@ def create_save_pdf(
dir_path='/static/uploads/pdf/temp/',
identifier=get_file_name(),
upload_dir='static/media/',
new_renderer=False,
extra_identifiers={},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not use mutable data structures for argument defaults. They are created during function definition time. All calls to the function reuse this one instance of that data structure, persisting changes between them.

):
"""
Create and Saves PDFs from html
Expand All @@ -365,12 +368,16 @@ def create_save_pdf(
filename = identifier + '.pdf'
dest = filedir + filename

file = open(dest, "wb")
pisa.CreatePDF(io.BytesIO(pdf_data.encode('utf-8')), file)
file.close()
pdf_content = pdf_data.encode('utf-8')
if not new_renderer:
file = open(dest, "wb")
pisa.CreatePDF(io.BytesIO(pdf_content), file)
file.close()
else:
HTML(string=pdf_content).write_pdf(dest)

uploaded_file = UploadedFile(dest, filename)
upload_path = key.format(identifier=identifier)
upload_path = key.format(**{'identifier': identifier, **extra_identifiers})
new_file = upload(uploaded_file, upload_path, upload_dir=upload_dir)
# Removing old file created
os.remove(dest)
Expand Down
215 changes: 19 additions & 196 deletions app/api/helpers/scheduled_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,20 @@
import logging

import pytz
from dateutil.relativedelta import relativedelta
from flask import render_template
from flask_celeryext import RequestContextTask
from sqlalchemy.orm.exc import NoResultFound

from app.api.helpers.db import save_to_db
from app.api.helpers.files import create_save_pdf
from app.api.helpers.mail import (
send_email_after_event,
send_email_for_monthly_fee_payment,
send_followup_email_for_monthly_fee_payment,
)
from app.api.helpers.notification import (
send_followup_notif_monthly_fee_payment,
send_notif_after_event,
send_notif_monthly_fee_payment,
)
from app.api.helpers.mail import send_email_after_event
from app.api.helpers.notification import send_notif_after_event
from app.api.helpers.query import get_user_event_roles_by_role_name
from app.api.helpers.storage import UPLOAD_PATHS
from app.api.helpers.utilities import make_dict, monthdelta
from app.api.helpers.utilities import make_dict
from app.instance import celery
from app.models import db
from app.models.event import Event
from app.models.event_invoice import EventInvoice
from app.models.order import Order
from app.models.session import Session
from app.models.speaker import Speaker
from app.models.ticket_fee import TicketFees, get_fee
from app.models.ticket_holder import TicketHolder
from app.settings import get_settings

Expand Down Expand Up @@ -90,118 +76,16 @@ def change_session_state_on_event_completion():
)


@celery.task(base=RequestContextTask, name='send.event.fee.notification')
def send_event_fee_notification():
from app.instance import current_app as app

with app.app_context():
events = Event.query.filter_by(deleted_at=None, state='published').all()
for event in events:
latest_invoice = (
EventInvoice.query.filter_by(event_id=event.id)
.order_by(EventInvoice.created_at.desc())
.first()
)

if latest_invoice:
orders = (
Order.query.filter_by(event_id=event.id)
.filter_by(status='completed')
.filter(Order.completed_at > latest_invoice.created_at)
.all()
)
else:
orders = (
Order.query.filter_by(event_id=event.id)
.filter_by(status='completed')
.all()
)

fee_total = 0
for order in orders:
for ticket in order.tickets:
if order.paid_via != 'free' and order.amount > 0 and ticket.price > 0:
fee = ticket.price * (
get_fee(event.payment_country, order.event.payment_currency)
/ 100.0
)
fee_total += fee

if fee_total > 0:
owner = get_user_event_roles_by_role_name(event.id, 'owner').first()
new_invoice = EventInvoice(
amount=fee_total, event_id=event.id, user_id=owner.user.id
)

if event.discount_code_id and event.discount_code:
r = relativedelta(datetime.datetime.utcnow(), event.created_at)
if r <= event.discount_code.valid_till:
new_invoice.amount = fee_total - (
fee_total * (event.discount_code.value / 100.0)
)
new_invoice.discount_code_id = event.discount_code_id

save_to_db(new_invoice)
prev_month = monthdelta(new_invoice.created_at, 1).strftime(
"%b %Y"
) # Displayed as Aug 2016
app_name = get_settings()['app_name']
frontend_url = get_settings()['frontend_url']
link = '{}/invoices/{}'.format(frontend_url, new_invoice.identifier)
send_email_for_monthly_fee_payment(
new_invoice.user.email,
event.name,
prev_month,
new_invoice.amount,
app_name,
link,
)
send_notif_monthly_fee_payment(
new_invoice.user,
event.name,
prev_month,
new_invoice.amount,
app_name,
link,
new_invoice.event_id,
)


@celery.task(base=RequestContextTask, name='send.event.fee.notification.followup')
def send_event_fee_notification_followup():
from app.instance import current_app as app

with app.app_context():
incomplete_invoices = EventInvoice.query.filter(
EventInvoice.status != 'paid'
EventInvoice.amount > 0, EventInvoice.status != 'paid'
).all()
for incomplete_invoice in incomplete_invoices:
if incomplete_invoice.amount > 0:
prev_month = monthdelta(incomplete_invoice.created_at, 1).strftime(
"%b %Y"
) # Displayed as Aug 2016
app_name = get_settings()['app_name']
frontend_url = get_settings()['frontend_url']
link = '{}/event-invoice/{}/review'.format(
frontend_url, incomplete_invoice.identifier
)
send_followup_email_for_monthly_fee_payment(
incomplete_invoice.user.email,
incomplete_invoice.event.name,
prev_month,
incomplete_invoice.amount,
app_name,
link,
)
send_followup_notif_monthly_fee_payment(
incomplete_invoice.user,
incomplete_invoice.event.name,
prev_month,
incomplete_invoice.amount,
app_name,
link,
incomplete_invoice.event.id,
)
incomplete_invoice.send_notification(follow_up=True)


@celery.task(base=RequestContextTask, name='expire.pending.tickets')
Expand Down Expand Up @@ -249,73 +133,18 @@ def delete_ticket_holders_no_order_id():
db.session.commit()


@celery.task(base=RequestContextTask, name='event.invoices.mark.due')
def event_invoices_mark_due():
db.session.query(EventInvoice).filter(
EventInvoice.status == 'upcoming',
Event.id == EventInvoice.event_id,
Event.ends_at >= datetime.datetime.now(),
(
EventInvoice.created_at + datetime.timedelta(days=30)
<= datetime.datetime.now()
),
).update({EventInvoice.status: 'due'}, synchronize_session=False)


@celery.task(base=RequestContextTask, name='send.monthly.event.invoice')
def send_monthly_event_invoice():
events = Event.query.filter_by(deleted_at=None, state='published').all()
events = Event.query.filter_by(deleted_at=None).filter(Event.owner != None).all()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comparison to None should be 'if cond is not None:'


for event in events:
# calculate net & gross revenues
user = event.owner
admin_info = get_settings()
currency = event.payment_currency
try:
ticket_fee_object = (
db.session.query(TicketFees).filter_by(currency=currency).one()
)
except NoResultFound:
logger.error('Ticket Fee not found for event id {id}'.format(id=event.id))
continue

ticket_fee_percentage = ticket_fee_object.service_fee
ticket_fee_maximum = ticket_fee_object.maximum_fee
orders = Order.query.filter_by(event=event).all()
gross_revenue = event.calc_monthly_revenue()
invoice_amount = gross_revenue * (ticket_fee_percentage / 100)
if invoice_amount > ticket_fee_maximum:
invoice_amount = ticket_fee_maximum
net_revenue = gross_revenue - invoice_amount
payment_details = {
'tickets_sold': event.tickets_sold,
'gross_revenue': gross_revenue,
'net_revenue': net_revenue,
'amount_payable': invoice_amount,
}
# save invoice as pdf
pdf = create_save_pdf(
render_template(
'pdf/event_invoice.html',
orders=orders,
user=user,
admin_info=admin_info,
currency=currency,
event=event,
ticket_fee_object=ticket_fee_object,
payment_details=payment_details,
net_revenue=net_revenue,
),
UPLOAD_PATHS['pdf']['event_invoice'],
dir_path='/static/uploads/pdf/event_invoices/',
identifier=event.identifier,
)
# save event_invoice info to DB

event_invoice = EventInvoice(
amount=invoice_amount, invoice_pdf_url=pdf, event_id=event.id
)
save_to_db(event_invoice)
event_invoice = EventInvoice(event=event)
pdf_url = event_invoice.populate()
if pdf_url:
save_to_db(event_invoice)
event_invoice.send_notification()
else:
logger.error('Error in generating event invoice for event %s', event)


@celery.on_after_configure.connect
Expand All @@ -325,23 +154,17 @@ def setup_scheduled_task(sender, **kwargs):
# Every day at 5:30
sender.add_periodic_task(crontab(hour=5, minute=30), send_after_event_mail)
# Every 1st day of month at 0:00
sender.add_periodic_task(
crontab(minute=0, hour=0, day_of_month=1), send_event_fee_notification
)
# sender.add_periodic_task(
# crontab(minute=0, hour=0, day_of_month=1), send_event_fee_notification_followup
# )
# Every 1st day of month at 0:00
sender.add_periodic_task(
crontab(minute=0, hour=0, day_of_month=1), send_event_fee_notification_followup
)
# sender.add_periodic_task(
# crontab(minute=0, hour=0, day_of_month=1), send_monthly_event_invoice
# )
# Every day at 5:30
sender.add_periodic_task(
crontab(hour=5, minute=30), change_session_state_on_event_completion
)
# Every 1st day of month at 0:00
sender.add_periodic_task(
crontab(minute=0, hour=0, day_of_month=1), send_monthly_event_invoice
)
# Every day at 5:00
sender.add_periodic_task(crontab(minute=0, hour=5), event_invoices_mark_due)
# Every 25 minutes
sender.add_periodic_task(crontab(minute='*/25'), expire_pending_tickets)
# Every 10 minutes
Expand Down
2 changes: 1 addition & 1 deletion app/api/helpers/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
'ticket_attendee': 'attendees/tickets/pdf/{identifier}',
'order': 'orders/invoices/pdf/{identifier}',
'tickets_all': 'orders/tickets/pdf/{identifier}',
'event_invoice': 'events/organizer/invoices/pdf/{identifier}',
'event_invoice': 'events/organizer/invoices/pdf/{event_identifier}/{identifier}',
},
}

Expand Down
2 changes: 1 addition & 1 deletion app/api/helpers/system_mails.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@
'subject': u'{date} - Monthly service fee invoice for {event_name}',
'message': (
u"The total service fee for the ticket sales of {event_name} in the month of {date} is {amount}."
+ u"<br/> That payment for the same has to be made in two weeks. <a href='{payment_url}'>Click here</a> to "
+ u"<br/> That payment for the same has to be made in 30 days. <a href='{payment_url}'>Click here</a> to "

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (118 > 90 characters)

u"view your invoice and complete the payment."
u"<br><br><em>Thank you for using {app_name}.</em>"
),
Expand Down
Loading