Created
September 5, 2017 22:43
-
-
Save ehnischiguti/04fa27d451cc0aa6e41984bf367fa3b8 to your computer and use it in GitHub Desktop.
Python script to simulate transaction in Pagar.me's environment.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
** Recipient_1 ** | |
+---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------+ | |
| installment | amount | fee | anticipation_fee | net_amount | duration | payment_date | original_payment_date | | |
|---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------| | |
| 1 | 3743 | 142 | 0 | 3601 | 0 | 2017-10-06 | 2017-10-06 | | |
| 2 | 3755 | 148 | 0 | 3607 | 0 | 2017-11-07 | 2017-11-07 | | |
| 3 | 3755 | 148 | 0 | 3607 | 0 | 2017-12-05 | 2017-12-05 | | |
| 4 | 3755 | 148 | 0 | 3607 | 0 | 2018-01-04 | 2018-01-04 | | |
| 5 | 3755 | 148 | 0 | 3607 | 0 | 2018-02-05 | 2018-02-05 | | |
| 6 | 3755 | 148 | 0 | 3607 | 0 | 2018-03-06 | 2018-03-06 | | |
| 7 | 3755 | 148 | 0 | 3607 | 0 | 2018-04-04 | 2018-04-04 | | |
| 8 | 3755 | 148 | 0 | 3607 | 0 | 2018-05-04 | 2018-05-04 | | |
| 9 | 3755 | 148 | 0 | 3607 | 0 | 2018-06-05 | 2018-06-05 | | |
| 10 | 3755 | 148 | 0 | 3607 | 0 | 2018-07-03 | 2018-07-03 | | |
| 11 | 3755 | 148 | 0 | 3607 | 0 | 2018-08-02 | 2018-08-02 | | |
+---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------+ | |
** Recipient_2 ** | |
+---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------+ | |
| installment | amount | fee | anticipation_fee | net_amount | duration | payment_date | original_payment_date | | |
|---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------| | |
| 1 | 5620 | 222 | 4 | 5394 | 1 | 2017-10-05 | 2017-10-06 | | |
| 2 | 5632 | 221 | 149 | 5262 | 33 | 2017-10-05 | 2017-11-07 | | |
| 3 | 5632 | 221 | 275 | 5136 | 61 | 2017-10-05 | 2017-12-05 | | |
| 4 | 5632 | 221 | 410 | 5001 | 91 | 2017-10-05 | 2018-01-04 | | |
| 5 | 5632 | 221 | 555 | 4856 | 123 | 2017-10-05 | 2018-02-05 | | |
| 6 | 5632 | 221 | 685 | 4726 | 152 | 2017-10-05 | 2018-03-06 | | |
| 7 | 5632 | 221 | 816 | 4595 | 181 | 2017-10-05 | 2018-04-04 | | |
| 8 | 5632 | 221 | 951 | 4460 | 211 | 2017-10-05 | 2018-05-04 | | |
| 9 | 5632 | 221 | 1096 | 4315 | 243 | 2017-10-05 | 2018-06-05 | | |
| 10 | 5632 | 221 | 1222 | 4189 | 271 | 2017-10-05 | 2018-07-03 | | |
| 11 | 5632 | 221 | 1357 | 4054 | 301 | 2017-10-05 | 2018-08-02 | | |
+---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------+ | |
** Recipient_3 ** | |
+---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------+ | |
| installment | amount | fee | anticipation_fee | net_amount | duration | payment_date | original_payment_date | | |
|---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------| | |
| 1 | 7506 | 0 | 0 | 7506 | 0 | 2017-10-06 | 2017-10-06 | | |
| 2 | 7508 | 0 | 0 | 7508 | 0 | 2017-11-07 | 2017-11-07 | | |
| 3 | 7508 | 0 | 0 | 7508 | 0 | 2017-12-05 | 2017-12-05 | | |
| 4 | 7508 | 0 | 0 | 7508 | 0 | 2018-01-04 | 2018-01-04 | | |
| 5 | 7508 | 0 | 0 | 7508 | 0 | 2018-02-05 | 2018-02-05 | | |
| 6 | 7508 | 0 | 0 | 7508 | 0 | 2018-03-06 | 2018-03-06 | | |
| 7 | 7508 | 0 | 0 | 7508 | 0 | 2018-04-04 | 2018-04-04 | | |
| 8 | 7508 | 0 | 0 | 7508 | 0 | 2018-05-04 | 2018-05-04 | | |
| 9 | 7508 | 0 | 0 | 7508 | 0 | 2018-06-05 | 2018-06-05 | | |
| 10 | 7508 | 0 | 0 | 7508 | 0 | 2018-07-03 | 2018-07-03 | | |
| 11 | 7508 | 0 | 0 | 7508 | 0 | 2018-08-02 | 2018-08-02 | | |
+---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------+ | |
** Recipient_4 ** | |
+---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------+ | |
| installment | amount | fee | anticipation_fee | net_amount | duration | payment_date | original_payment_date | | |
|---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------| | |
| 1 | 11259 | 435 | 0 | 10824 | 0 | 2017-10-06 | 2017-10-06 | | |
| 2 | 11262 | 443 | 0 | 10819 | 0 | 2017-11-07 | 2017-11-07 | | |
| 3 | 11262 | 443 | 0 | 10819 | 0 | 2017-12-05 | 2017-12-05 | | |
| 4 | 11262 | 443 | 0 | 10819 | 0 | 2018-01-04 | 2018-01-04 | | |
| 5 | 11262 | 443 | 0 | 10819 | 0 | 2018-02-05 | 2018-02-05 | | |
| 6 | 11262 | 443 | 0 | 10819 | 0 | 2018-03-06 | 2018-03-06 | | |
| 7 | 11262 | 443 | 0 | 10819 | 0 | 2018-04-04 | 2018-04-04 | | |
| 8 | 11262 | 443 | 0 | 10819 | 0 | 2018-05-04 | 2018-05-04 | | |
| 9 | 11262 | 443 | 0 | 10819 | 0 | 2018-06-05 | 2018-06-05 | | |
| 10 | 11262 | 443 | 0 | 10819 | 0 | 2018-07-03 | 2018-07-03 | | |
| 11 | 11262 | 443 | 0 | 10819 | 0 | 2018-08-02 | 2018-08-02 | | |
+---------------+----------+-------+--------------------+--------------+------------+----------------+-------------------------+ | |
** Total ** | |
+-------------+----------+-------+--------------------+--------------+ | |
| | amount | fee | anticipation_fee | net_amount | | |
|-------------+----------+-------+--------------------+--------------| | |
| Recipient_1 | 41293 | 1622 | 0 | 39671 | | |
| Recipient_2 | 61940 | 2432 | 7520 | 51988 | | |
| Recipient_3 | 82586 | 0 | 0 | 82586 | | |
| Recipient_4 | 123879 | 4865 | 0 | 119014 | | |
| Total | 309698 | 8919 | 7520 | 293259 | | |
+-------------+----------+-------+--------------------+--------------+ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Copyright (c) 2017-present Pagar.me Pagamentos S.A. | |
Licensed under the Apache License, Version 2.0 (the "License"); you may not use | |
this library except in compliance with the License. You may obtain a copy of | |
the License at | |
www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software distributed | |
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR | |
CONDITIONS OF ANY KIND, either express or implied. See the License for the | |
specific language governing permissions and limitations under the License. | |
""" | |
# 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() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment