# Requirements: pandas, tabulate
from tabulate import tabulate
from pandas import DataFrame
from datetime import (
datetime, timedelta)
import math
class Payable(object):
def __init__(self, original_payment_date, amount, fee, installment, recipient_id):
self.original_payment_date = original_payment_date
self.payment_date = original_payment_date
self.amount = amount
self.fee = fee
self.installment = installment
self.recipient_id = recipient_id
self.anticipation_fee = 0
self.anticipated = False
self.duration = 0
self.rebalance()
def anticipate(self, anticipation_fee, anticipation_date):
"""
Calculate anticipation fee when anticipating
Can't anticipate if anticipation_date > original_payment_date
"""
if anticipation_date >= self.original_payment_date:
return
self.duration = (self.original_payment_date - anticipation_date).days
self.anticipation_fee = round(
(self.duration/30) * (anticipation_fee/100) * (self.amount - self.fee))
self.payment_date = anticipation_date
self.anticipated = True
self.rebalance()
def rebalance(self):
"""
Calculate payable net amount
"""
self.net_amount = self.amount - self.fee - self.anticipation_fee
def describe(self):
return dict(
original_payment_date=self.original_payment_date,
payment_date=self.payment_date,
amount=self.amount,
fee=self.fee,
installment=self.installment,
recipient_id=self.recipient_id,
anticipation_fee=self.anticipation_fee,
anticipated=self.anticipated,
duration=self.duration,
net_amount=self.net_amount)
class Transaction(object):
def __init__(self, split_rules, date, amount, installments, mdr):
self.split_rules = split_rules
self.date = date
self.amount = amount
self.installments = installments
self.mdr = mdr
self.calculate_fee()
self.calculate_recipients_amounts()
self.calculate_payables()
def calculate_fee(self):
"""
Calculate transaction fee
"""
self.fee = round(self.amount * self.mdr / 100)
def calculate_recipients_amounts(self):
"""
Calculate amount and fee to be received by each recipient
Remainder values are added to charge remainder
"""
self.recipients_amounts = {}
amount_remainder = self.amount
fee_remainder = self.fee
for recipient in self.split_rules.recipients:
recipient_id = recipient.recipient_id
self.recipients_amounts[recipient_id] = {}
recipient_amount = round(
self.amount * recipient.percentage / self.split_rules.cumulated_percentage)
self.recipients_amounts[recipient_id]["amount"] = recipient_amount
amount_remainder -= recipient_amount
if recipient.charge_processing_fee is True:
recipient_fee = round(
self.fee * recipient.percentage / self.split_rules.fee_cumulated_percentage)
else:
recipient_fee = 0
self.recipients_amounts[recipient_id]["fee"] = recipient_fee
fee_remainder -= recipient_fee
charge_remainder_recipient = self.split_rules.get_charge_remainder()
self.recipients_amounts[charge_remainder_recipient][
"amount"] += amount_remainder
self.recipients_amounts[charge_remainder_recipient][
"fee"] += fee_remainder
def calculate_payables(self):
"""
Create a payable for each recipient and each installment
"""
self.payables = []
payment_dates = calculate_dates(self.date, self.installments)
for recipient_id, recipient_amounts in self.recipients_amounts.items():
amount = recipient_amounts["amount"]
fee = recipient_amounts["fee"]
amount_divisions = calculate_installments(
amount, self.installments)
fee_divisions = calculate_installments(fee, self.installments)
for installment in range(1, self.installments + 1):
installment_payment_date = payment_dates[installment]
installment_amount = amount_divisions[installment]
installment_fee = fee_divisions[installment]
self.payables.append(
Payable(installment_payment_date, installment_amount, installment_fee, installment, recipient_id))
class Recipient(object):
def __init__(self, recipient_id, percentage, charge_processing_fee, charge_remainder):
self.recipient_id = recipient_id
self.percentage = percentage
self.charge_processing_fee = charge_processing_fee
self.charge_remainder = charge_remainder
class SplitRules(object):
def __init__(self):
self.recipients = []
self.cumulated_percentage = None
self.fee_cumulated_percentage = None
def add_recipient(self, recipient_id, percentage, charge_processing_fee, charge_remainder):
"""
Add recipient to split rule
"""
self.recipients.append(
Recipient(recipient_id, percentage, charge_processing_fee, charge_remainder))
self.rebalance()
def rebalance(self):
"""
Save cumulated percentage and fee cumulated percentage
to normalize distribution
"""
self.cumulated_percentage = sum(
[recipient.percentage for recipient in self.recipients])
self.fee_cumulated_percentage = sum(
[recipient.percentage for recipient in self.recipients if recipient.charge_processing_fee is True])
def get_charge_remainder(self):
return (next(filter(lambda recipient: recipient.charge_remainder is True, self.recipients))).recipient_id
class Company(object):
def __init__(self, anticipation_fee, mdrs):
self.anticipation_fee = anticipation_fee
self.mdrs = mdrs
def get_mdr(self, installments):
"""
Return the mdr to be applied given the number of installments
"""
if installments == 1:
return self.mdrs[1]
elif installments in range(2, 7):
return self.mdrs[2]
else:
return self.mdrs[7]
def create_transaction(self, split_rules, date, amount, installments):
mdr = self.get_mdr(installments)
return Transaction(split_rules, date, amount, installments, mdr)
def anticipate_payables(self, anticipation_date, payables):
for payable in payables:
payable.anticipate(self.anticipation_fee, anticipation_date)
def calculate_installments(amount, installments):
"""
Given an amount and a number of installments,
returns the amount distribution on each installment
"""
amounts = {}
base_amount = math.floor(amount / installments)
remainder = (amount % installments)
if remainder <= 1:
for installment in range(1, installments + 1):
amounts[installment] = base_amount + \
(remainder if installment == 1 else 0)
return amounts
missing_cents = remainder % (installments - 1)
if (missing_cents < (installments - 1)):
missing_cents = installments - 1 - missing_cents
other_installments = (missing_cents + remainder) / (installments - 1)
for installment in range(1, installments + 1):
amounts[installment] = int(
base_amount + (-1 * missing_cents if installment == 1 else other_installments))
return amounts
def calculate_dates(initial_date, installments):
"""
Calculate payment dates given an initial date and the
number of installments
"""
dates = {}
for offset, installment in enumerate(range(1, installments + 1)):
base_date = initial_date + timedelta(offset * 30 + 29)
final_date = add_business_days(base_date, 2)
dates[installment] = final_date
return dates
def is_holiday(date):
"""
Checks if a given date is holiday in Brazil
"""
if not isinstance(date, str):
date = date.strftime("%Y-%m-%d")
holidays = [
"2017-01-01", "2017-02-27", "2017-02-28", "2017-04-14", "2017-04-21", "2017-05-01", "2017-06-15",
"2017-07-09", "2017-09-07", "2017-10-12", "2017-11-02", "2017-11-15", "2017-11-20", "2017-12-25",
"2017-12-29", "2018-01-01", "2018-01-25", "2018-02-12", "2018-02-13", "2018-03-30", "2018-04-21",
"2018-05-01", "2018-05-31", "2018-07-09", "2018-09-07", "2018-10-12", "2018-11-02", "2018-11-15",
"2018-11-20", "2018-12-25", "2018-12-31", "2019-01-01", "2019-01-25", "2019-03-04", "2019-03-05",
"2019-04-19", "2019-04-21", "2019-05-01", "2019-06-20", "2019-07-09", "2019-09-07", "2019-10-12",
"2019-11-02", "2019-11-15", "2019-11-20", "2019-12-25", "2019-12-31", "2020-01-01", "2020-01-25",
"2020-02-24", "2020-02-25", "2020-04-10", "2020-04-21", "2020-05-01", "2020-06-11", "2020-07-09",
"2020-09-07", "2020-10-12", "2020-11-02", "2020-11-15", "2020-11-20", "2020-12-25", "2020-12-31"]
return (date in holidays)
def add_business_days(date, days):
"""
Returns date after a given number of business days
"""
count = 0
while count < days:
date += timedelta(1)
if (date.weekday() not in [5, 6]) and (not is_holiday(date)):
count += 1
return date
def process_dataframe(title, dataframe, print_report, save_report):
table = tabulate(dataframe, headers='keys', tablefmt='psql')
if print_report is True:
print("\n ** {} **\n".format(title))
print(table)
if save_report is True:
with open("transaction_report.txt", "a") as report_file:
report_file.write("\n ** {} **\n".format(title))
report_file.write(table + "\n")
def report(transaction, print_report=True, save_report=False):
payables = transaction.payables
recipients = {}
for payable in payables:
recipient_id = payable.recipient_id
if recipient_id in recipients:
recipients[recipient_id].append(payable)
else:
recipients[recipient_id] = [payable]
total_df = DataFrame()
for recipient_id in sorted(recipients.keys()):
recipient_payables = recipients[recipient_id]
df = DataFrame([payable.describe() for payable in recipient_payables])
df = df.set_index('installment')
df = df[["amount", "fee", "anticipation_fee", "net_amount",
"duration", "payment_date", "original_payment_date"]]
process_dataframe(recipient_id, df, print_report, save_report)
sum_row = {col: df[col].sum() for col in [
"amount", "fee", "anticipation_fee", "net_amount"]}
sum_df = DataFrame(sum_row, index=[recipient_id])
total_df = total_df.append(sum_df)
sum_row = {col: total_df[col].sum() for col in [
"amount", "fee", "anticipation_fee", "net_amount"]}
sum_df = DataFrame(sum_row, index=["Total"])
total_df = total_df.append(sum_df)
total_df = total_df[["amount", "fee", "anticipation_fee", "net_amount"]]
process_dataframe("Total", total_df, print_report, save_report)
def main():
"""
Create company, set anticipation and mdr fees
"""
company = Company(
anticipation_fee=2.5,
mdrs={
1: 2.25, # for 1 installment
2: 2.50, # between 2-6 installments
7: 2.88 # 7+ installments
})
"""
Create Split Rule and add multiple recipients configurations
Cumulated percentage is not necessarily 100 once it will be normalized
At least one recipient must have charge_processing_fee = True
Only one recipient can have charge_remainder = True
"""
split_rules = SplitRules()
split_rules.add_recipient(
recipient_id="Recipient_1",
percentage=20,
charge_processing_fee=True,
charge_remainder=False)
split_rules.add_recipient(
recipient_id="Recipient_2",
percentage=30,
charge_processing_fee=True,
charge_remainder=False)
split_rules.add_recipient(
recipient_id="Recipient_3",
percentage=40,
charge_processing_fee=False,
charge_remainder=False)
split_rules.add_recipient(
recipient_id="Recipient_4",
percentage=60,
charge_processing_fee=True,
charge_remainder=True)
"""
Create a transaction on current date
using company and split rules
"""
current_date = datetime.now().date()
transaction = company.create_transaction(
split_rules=split_rules,
date=current_date,
amount=309698,
installments=11)
"""
Simulate anticipation for Recipient_2 to
current date + 29 days + 1 business day
"""
recipient2_payables = list(filter(
lambda payable: payable.recipient_id == "Recipient_2",
transaction.payables))
anticipation_date = add_business_days(current_date + timedelta(29), 1)
company.anticipate_payables(anticipation_date, recipient2_payables)
report(transaction, print_report=True, save_report=True)
if __name__ == "__main__":
main()

**What I have tried:**
i dont know python so i am unable to understand this code for my assignment