Skip to content

Instantly share code, notes, and snippets.

@ehnischiguti
Created September 5, 2017 22:43
Show Gist options
  • Save ehnischiguti/04fa27d451cc0aa6e41984bf367fa3b8 to your computer and use it in GitHub Desktop.
Save ehnischiguti/04fa27d451cc0aa6e41984bf367fa3b8 to your computer and use it in GitHub Desktop.
Python script to simulate transaction in Pagar.me's environment.
** 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 |
+-------------+----------+-------+--------------------+--------------+
"""
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