diff --git a/Dockerfile b/Dockerfile index 0c6ffa6..b60507b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,10 @@ FROM python:3.8-slim-buster +WORKDIR /app + COPY requirements.txt requirements.txt RUN pip3 install -r requirements.txt COPY mortgage/ . -CMD [ "python3", "-m" , "web", "run", "--host=0.0.0.0"] \ No newline at end of file +CMD [ "python3", "-m" , "web", "run", "--host=0.0.0.0"] diff --git a/mortgage/web_ng.py b/mortgage/web_ng.py deleted file mode 100644 index a9fd4f8..0000000 --- a/mortgage/web_ng.py +++ /dev/null @@ -1,544 +0,0 @@ -import json -import jinja2 -import smtplib -import os -from flask import Flask, render_template, request, redirect -from decimal import * -from datetime import * -from fpdf import FPDF, HTMLMixin -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText -from email.mime.base import MIMEBase -from email import encoders - -module_directory = os.path.dirname(__file__) -app_directory = os.path.normpath(os.path.join(module_directory, "..")) -loader = jinja2.FileSystemLoader([os.path.join(module_directory, "templates")]) -environment = jinja2.Environment(loader=loader) -app = Flask(__name__) - - -###### -# Flask Call Backs -###### - - -@app.route('/') -def hello(): - loans = getLoanFiles(app_directory) - - #if a loan was not specified, choose the first loan and reload page with it - if 'loan' in request.args: - filename = request.args["loan"] - else: - return redirect('/?loan=' + loans[0]['filename']) - - loan = loadLoanInformation(getFullPathofLoanFile(filename)) - amortizeLoan(loan) - - return render_template('main.html', filename=filename, loans=loans, model=loan) - - -@app.route('/update_file', methods=['POST']) -def update_file(): - messages = [] - - loanFile = request.form["loan"] - data = getDatastore(getFullPathofLoanFile(loanFile)) - - late_fee = Decimal('0.00').quantize(Decimal('1.00')) - extra_payment = False - payment_amount = Decimal('0.00').quantize(Decimal('1.00')) - payment_history = data["payments"] - todays_date = str(datetime.now().strftime("%Y-%m-%d")) - if 'date' in request.form: - if request.form['date'] == '': - payment_date = todays_date - messages.append("No date was provided. Assuming today's date of " + payment_date + ".") - else: - payment_date = request.form['date'] - messages.append("Date provided: " + payment_date + ".") - else: - payment_date = todays_date - messages.append("No date was provided. Assuming today's date of " + payment_date + ".") - - proceed_flag = True - if 'amount' in request.form: - if request.form['amount'] != '': - try: - payment_amount = Decimal(request.form['amount']) - messages.append("Amount provided: " + str(payment_amount) + ".") - except: - messages.append("The amount provided could not be interpreted. Your payment was not recorded.") - proceed_flag = False - else: - pass - - if 'latefee' in request.form: - if request.form['latefee'] != '': - try: - late_fee = Decimal(request.form['latefee']) - messages.append("Late fee amount: " + str(late_fee) + ".") - except: - messages.append("The late fee amount was specified but could not be interpreted. " + - "Your payment was not recorded.") - proceed_flag = False - - if 'extra' in request.form: - if request.form['extra'] != '': - extra_payment = True - - if proceed_flag is True: - try: - backup_filename = loanFile + ".backup-" + datetime.now().strftime("%Y-%m-%d %H-%M-%S") + ".json" - backup_file = open(getFullPathofLoanFile(backup_filename), 'w+') - json.dump(data, backup_file, indent=2) - backup_file.close() - except: - messages.append("A backup file could not be created. Your payment was not recorded.") - proceed_flag = False - else: - messages.append("A backup of your file was created: '" + backup_filename + "'") - - if proceed_flag is True: - try: - payment_history.append([payment_date, str(payment_amount), str(late_fee), str(extra_payment)]) - file = open(getFullPathofLoanFile(loanFile), 'w+') - json.dump(data, file, indent=2) - file.close() - except: - messages.append("An error occurred writing to the file. Your payment file may be corrupt, " + - "please consider rolling back to the backup created above.") - else: - messages.append("The payment was successfully written. ") - - # see if an email notification was requested, if not, exit now - if 'notify' not in request.form: - messages.append("Payment notification email not requested.") - return render_template('add.html', filename=loanFile, messages=messages) - - messages.append("Payment notification email requested.") - # send email - emailParameters = data["email"] - result = {} - payment = {} - result['payment'] = payment - payment['today'] = todays_date - payment['date'] = payment_date - payment['payer'] = data['borrower']['name'] - payment['payee'] = data['lender']['name'] - payment['amount'] = payment_amount - payment['late_fee'] = late_fee - payment['extra_payment'] = extra_payment - - subject = "Your loan payment has been received" - html = transformTemplate(selectTemplate('paymentNotification'), result) - msg = generatePaymentNotificationEmail(emailParameters["from_address"], emailParameters["to_address"], - subject, html) - sendEmail(msg, emailParameters["from_address"], emailParameters["to_address"], emailParameters['password']) - return render_template('add.html', filename=loanFile, messages=messages) - - -@app.route('/send_statement', methods=['POST']) -def send_statement(): - loanFile = request.form["loan"] - subject = request.form["subject"] - message = request.form["message"] - - loan = loadLoanInformation(getFullPathofLoanFile(loanFile)) - amortizeLoan(loan) - - reportCreated = False - textReport = pdfReport = htmlReport = None - - if 'text' in request.form: - textReport = transformTemplate(selectTemplate('text'), loan) - reportCreated = True - - if 'pdf' in request.form: - pdfInterimReport = transformTemplate(selectTemplate('pdf'), loan) - pdfReport = createPDF(pdfInterimReport) - reportCreated = True - - if ('html' in request.form) or (reportCreated is False): - htmlReport = transformTemplate(selectTemplate('html'), loan) - - # send email - emailParameters = loan["email"] - - msg = generateStatementEmail(emailParameters["from_address"], - emailParameters["to_address"], - subject, - message, pdfReport, htmlReport, textReport) - - sendEmail(msg, emailParameters["from_address"], emailParameters["to_address"], emailParameters['password']) - - return render_template('email.html', filename=loanFile) - - -###### -# Loan File Functions -###### - -def getLoanFiles(directory): - loans = [] - count = 0 - - directory = os.path.normpath(directory) - print("Directory {}".format(directory)) - for filename in os.listdir(directory): - if filename.endswith(".loan"): - count=count+1 - loans.append(createLoanEntryMenuItem("{0} {1}".format(count, filename.removesuffix(".loan")), filename)) - - return loans - - -def createLoanEntryMenuItem(loanName, fileName): - x = {} - x['name'] = loanName - x['filename'] = fileName - return x - - -def getFullPathofLoanFile(filename): - app_path = os.path.dirname(__file__) - file_path = os.path.normpath(os.path.join(os.path.dirname(__file__),'..')) - print("App Path: {0}\nFile Path: {1}".format(app_path, file_path)) - return os.path.join(file_path, filename) - - -###### -# Datastore Manipulation Functions -###### - - -def getStatementHeader(datastore): - return datastore['header'] - - -def getEmailInformation(datastore): - return datastore['email'] - - -def loadLoanInformation(filename): - datastore = getDatastore(filename) - - loanModel = {} - loanModel['datastore'] = datastore - loanModel['email'] = getEmailInformation(datastore) - loanModel['parameters'] = getLoanParameters(datastore) - loanModel['lender'] = getLender(datastore) - loanModel['borrower'] = getBorrower(datastore) - loanModel['header'] = getStatementHeader(datastore) - return loanModel - - -def getLoanParameters(datastore): - # read in the loan profile information - loan = datastore['parameters'] - - annual_rate = Decimal(loan['interest_rate']) / 100 - daily_interest_rate = annual_rate / 360 - principal = Decimal(loan["principal"]).quantize(Decimal("1.00")) - periods_per_year = Decimal(loan["periods_per_year"]) - total_periods = Decimal(loan["periods"]) - #payment_day_of_month = int(loan['payment_day_of_month']) - - if "monthly_payment" in loan: - monthly_payment = Decimal(loan["monthly_payment"]).quantize(Decimal("1.00")) - else: - # calculate expected monthly payment - periodic_rate = annual_rate / periods_per_year - discount_factor = (((1 + periodic_rate) ** total_periods) - 1) / ( - periodic_rate * ((1 + periodic_rate) ** total_periods)) - monthly_payment = (principal / discount_factor).quantize(Decimal("1.00")) - - loan['principal'] = principal # standardizes the format - loan['annual_rate'] = annual_rate # standardizes the format - loan['daily_interest_rate'] = daily_interest_rate - loan['rate'] = '' + (annual_rate * 100).__str__() + '%' - loan['next_payment_amt'] = 0 - loan['next_payment_date'] = '12/12/12' - loan['total_periods'] = total_periods - loan['monthly_payment'] = monthly_payment - datastore['parameters'] = loan - return loan - - -def getLender(datastore): - return datastore['lender'] - - -def getBorrower(datastore): - return datastore['borrower'] - - -def getDatastore(filename=None): - try: - if filename: - with open(filename, 'r') as f: - datastore = json.load(f) - - except Exception as e: - print("An error occurred opening your loan file '%s'. " % filename) - print("The Exception:") - print(e.__repr__()) - quit() - - return datastore - -###### -# Loan Calculation Functions -###### - -def new_amortizeLoan(loan): - #loop over the historical data and re-calculate the actual amortization - monthly_payment = loan["parameters"]["monthly_payment"] - first_day_interest_accrues = datetime.strptime(loan["parameters"]["start_interest_date"], "%Y-%m-%d").date() - next_payment_date = datetime.strptime(loan["parameters"]["first_payment_month"], '%Y-%m-%d').date() - - - -def amortizeLoan(loan): - # loop over the payments and calculate the actual amortization - monthly_payment = loan["parameters"]["monthly_payment"] - - actual_payments = loan["datastore"]["payments"] - #invoices = loan["datastore"]["invoices"] - #payments = loan["datastore"]["payments"] - #allocations = loan["datastore"]["allocations"] - - - remaining_principal = loan["parameters"]["principal"] - payment_day_of_month = int(loan["parameters"]["payment_day_of_month"]) - daily_interest_rate = loan["parameters"]["daily_interest_rate"] - total_periods = loan["parameters"]["total_periods"] - interest_paid_through_date = datetime.strptime(loan["parameters"]["start_interest_date"], "%Y-%m-%d").date() - next_payment_date = datetime.strptime(loan["parameters"]["first_payment_month"], '%Y-%m-%d').date() - next_bill_date = date(year=next_payment_date.year, month=next_payment_date.month, day=payment_day_of_month) - - payment_number = 1 - annual_interest = 0 - total_interest = 0 - #old_bill_date = next_bill_date - #current_year = next_bill_date.year - - past_payments = [] - future_payments = [] - - for payment in actual_payments: - payment_date = datetime.strptime((payment[0]), '%Y-%m-%d').date() - payment_amount = Decimal(payment[1]).quantize(Decimal("1.00")) - days_since_last_payment = (payment_date - interest_paid_through_date).days - if len(payment) > 2: - late_fee = Decimal(payment[2]).quantize(Decimal("1.00")) - else: - late_fee = Decimal("0.00") - - # check for out of order payments, generally a sign of a data entry problem, especially years - if days_since_last_payment < 0: - print( - "Payment Number %s appears out of order. The payment date '%s' is before the previous payment on '%s'." - % (payment_number, payment_date, interest_paid_through_date)) - quit() - - new_interest = (days_since_last_payment * remaining_principal * daily_interest_rate).quantize(Decimal("0.00")) - new_principal = payment_amount - new_interest - late_fee - interest_paid_through_date = payment_date - total_interest = total_interest + new_interest - annual_interest = annual_interest + new_interest - remaining_principal = remaining_principal - new_principal - - # create the payment record for the template to render - payment_record = {} - payment_record['year'] = next_bill_date.year - payment_record['month'] = next_bill_date.month - payment_record['payment_number'] = payment_number - payment_record['bill_date'] = next_bill_date - payment_record['payment_date'] = payment_date - payment_record['days_of_interest'] = days_since_last_payment - payment_record['payment_amount'] = payment_amount - payment_record['principal_payment'] = payment_amount - new_interest - late_fee - payment_record['interest_payment'] = new_interest - payment_record['new_balance'] = remaining_principal - payment_record['interest_to_date'] = total_interest - payment_record['annual_interest_to_date'] = annual_interest - payment_record['late_fee'] = late_fee - past_payments.append(payment_record) - - payment_number = payment_number + 1 - - print("Payment record length: " + str(len(payment))) - #check for the extra payment flag, if its there, don't advance the next payment date - if len(payment) > 3 and payment[3] == "extra": - print("Extra payment flag: " + payment[3]) - #this is an extra payment don't advance the date - else: - old_bill_date = next_bill_date - - if old_bill_date.month < 12: - next_bill_date = date(year=old_bill_date.year, month=old_bill_date.month + 1, day=payment_day_of_month) - else: - annual_interest = Decimal("0.00") - next_bill_date = date(year=old_bill_date.year + 1, month=1, day=payment_day_of_month) - - loan["parameters"]["next_due_date"] = next_bill_date - - loan["total_interest_paid_to_date"] = total_interest - - if (remaining_principal < monthly_payment): - loan["parameters"]["next_payment_amt"] = remaining_principal - else: - loan["parameters"]["next_payment_amt"] = monthly_payment - - # loop over remaining scheduled payments and present estimated amortization - while (payment_number <= total_periods) and (remaining_principal > 0): - days_since_last_payment = (next_bill_date - interest_paid_through_date).days - new_interest = (days_since_last_payment * remaining_principal * daily_interest_rate).quantize(Decimal("0.00")) - - # make sure the last payment isn't too much - if new_interest + remaining_principal < monthly_payment: - monthly_payment = new_interest + remaining_principal - - new_principal = monthly_payment - new_interest - - remaining_principal = remaining_principal - new_principal - - interest_paid_through_date = next_bill_date - - # complete the future payment amortization record - future_payment_record = {} - future_payment_record['payment_number'] = payment_number - future_payment_record['payment_date'] = next_bill_date - future_payment_record['days_of_interest'] = days_since_last_payment - future_payment_record['payment_amount'] = monthly_payment - future_payment_record['principal_payment'] = new_principal - future_payment_record['interest_payment'] = new_interest - future_payment_record['new_balance'] = remaining_principal - future_payments.append(future_payment_record) - - payment_number = payment_number + 1 - old_bill_date = next_bill_date - if old_bill_date.month < 12: - next_bill_date = date(year=old_bill_date.year, month=old_bill_date.month + 1, day=payment_day_of_month) - else: - next_bill_date = date(year=old_bill_date.year + 1, month=1, day=payment_day_of_month) - - loan["balloon_payment"] = remaining_principal - loan["past_payments"] = past_payments - loan["future_payments"] = future_payments - return - - -###### -# Report Generation Functions -###### - - -def transformTemplate(template_fileName, loanModel): - # template_filename = "statement.text.jinja" - # setup jinja for creating the statement - template = environment.get_template(template_fileName) - - print(loanModel) - report = template.render(model=loanModel) - return report - - -def generateStatementEmail(from_address, to_address, subject, body, pdf, html, txt): - msg = MIMEMultipart() - msg['Subject'] = subject - msg['From'] = from_address - msg['To'] = to_address - - msg.attach(MIMEText(body)) - - if pdf is not None: - part = MIMEBase("application", "octet-stream") - part.set_payload(pdf) - encoders.encode_base64(part) - part.add_header('Content-Disposition', 'attachment; filename="statement.pdf"') - msg.attach(part) - - if html is not None: - part = MIMEBase("text", "html") - part.set_payload(html) - encoders.encode_base64(part) - part.add_header('Content-Disposition', 'attachment; filename="statement.html"') - msg.attach(part) - - if txt is not None: - part = MIMEBase("text", "plain") - part.set_payload(txt) - encoders.encode_base64(part) - part.add_header('Content-Disposition', 'attachment; filename="statement.txt"') - msg.attach(part) - - return msg - - -def generatePaymentNotificationEmail(from_address, to_address, subject, html): - msg = MIMEMultipart() - msg['Subject'] = subject - msg['From'] = from_address - msg['To'] = to_address - - #msg.attach(MIMEText(body)) - part = MIMEBase("text", "html") - part.set_payload(html) - encoders.encode_base64(part) - part.add_header('Content-Disposition', 'attachment; filename="notification.html"') - msg.attach(part) - - return msg - - -def sendEmail(msg, from_address, to_address, passwd): - try: - server = smtplib.SMTP('smtp.gmail.com', 587) - server.ehlo() - server.starttls() - server.login(from_address, passwd) - - server.sendmail(from_address, to_address, msg.as_string()) - server.close() - except: - print("Couldn't send email.") - - -def createPDF(report): - # create pdf - class MyFPDF(FPDF, HTMLMixin): - pass - - pdf = MyFPDF() - pdf.set_font(family='Arial', size=12) - pdf.add_page() - pdf.write_html(report) - return pdf.output(dest='S') - - -def selectTemplate(format): - if format == 'paymentNotification': - return 'payment_received_email.html.jinja' - - if format == 'html': - return 'statement.html.jinja' - - if format == 'pdf': - return 'statement.pdf.jinja' - - if format == 'text': - return 'statement.text.jinja' - - return 'statement.html.jinja' - - -def main(): - app.debug = True - app.run() - - -if __name__ == '__main__': - main()