Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Empty file.
156 changes: 156 additions & 0 deletions thebook/integrations/paypal/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import datetime
import decimal

import jmespath
import requests

from django.conf import settings
from django.contrib.auth import get_user_model

from thebook.bookkeeping.models import BankAccount, Category, Transaction


def _get_paypal_access_token():
response = requests.post(
f"{settings.PAYPAL_API_BASE_URL}/v1/oauth2/token",
data={
"grant_type": "client_credentials",
},
auth=(settings.PAYPAL_CLIENT_ID, settings.PAYPAL_CLIENT_SECRET),
)
auth_data = response.json()
return response.json().get("access_token") or ""


def _get_subscription(billing_agreement_id, access_token=None):
if access_token is None:
access_token = _get_paypal_access_token()
response = requests.get(
f"{settings.PAYPAL_API_BASE_URL}/v1/billing/subscriptions/{billing_agreement_id}",
headers={"Authorization": f"Bearer {access_token}"},
)
subscription = response.json()
return subscription


def fetch_transactions(start_date: datetime.date, end_date: datetime.date):
results = []

bank_account, _ = BankAccount.objects.get_or_create(
name=settings.PAYPAL_BANK_ACCOUNT
)
bank_fee_category, _ = Category.objects.get_or_create(
name=settings.BANK_FEE_CATEGORY_NAME
)
bank_account_transfer_category, _ = Category.objects.get_or_create(
name="Transferência entre contas bancárias"
)

user = get_user_model().objects.get_or_create_automation_user()
access_token = _get_paypal_access_token()

params = {
"start_date": start_date.strftime("%Y-%m-%dT00:00:00-00:00"),
"end_date": end_date.strftime("%Y-%m-%dT23:59:59-00:00"),
}
url = f"{settings.PAYPAL_API_BASE_URL}/v1/reporting/transactions"
response = requests.get(
url,
headers={"Authorization": f"Bearer {access_token}"},
params=params,
)
data = response.json()

for transaction in jmespath.search("transaction_details", data) or []:
transaction_status = jmespath.search(
"transaction_info.transaction_status", transaction
)
if transaction_status != "S":
# https://developer.paypal.com/docs/api/transaction-search/v1/#search_get!in=query&path=transaction_status&t=request
continue

transaction_id = jmespath.search("transaction_info.transaction_id", transaction)
if Transaction.objects.filter(reference=transaction_id).exists():
continue

transaction_currency_code = jmespath.search(
"transaction_info.transaction_amount.currency_code", transaction
)
if transaction_currency_code == "USD":
# TODO - Process USD transactions
continue
transaction_amount = decimal.Decimal(
jmespath.search("transaction_info.transaction_amount.value", transaction)
)

raw_date = jmespath.search(
"transaction_info.transaction_initiation_date", transaction
)
transaction_date = datetime.datetime.strptime(
raw_date, "%Y-%m-%dT%H:%M:%SZ"
).date()
transaction_fee_amount = decimal.Decimal(
jmespath.search("transaction_info.fee_amount.value", transaction) or "0"
)

transaction_type = jmespath.search(
"transaction_info.transaction_event_code", transaction
)
is_bank_account_transfer = transaction_type in ("T0400", "T0403")
if is_bank_account_transfer:
transaction_category = bank_account_transfer_category
transaction_description = (
f"Transferência entre contas bancárias - {transaction_id}"
)
else:
transaction_category = None
transaction_description = jmespath.search(
"transaction_info.transaction_subject", transaction
)

if transaction_type == "T0002":
# This flow only applies to Subscription payment
# https://developer.paypal.com/docs/transaction-search/transaction-event-codes/
paypal_reference_id = jmespath.search(
"transaction_info.paypal_reference_id", transaction
)
subscription = _get_subscription(paypal_reference_id, access_token)
given_name = (
jmespath.search("subscriber.name.given_name", subscription) or ""
)
surname = jmespath.search("subscriber.name.surname", subscription) or ""
full_name = " ".join([given_name, surname]).strip()
payer_id = jmespath.search("subscriber.payer_id", subscription) or ""
description_parts = (full_name, payer_id)
transaction_description = " - ".join(
[part for part in description_parts if part]
)

results.append(
Transaction(
reference=transaction_id,
date=transaction_date,
description=transaction_description,
amount=transaction_amount,
bank_account=bank_account,
category=transaction_category,
source="paypal-fetch-transactions",
created_by=user,
)
)

if not is_bank_account_transfer:
results.append(
Transaction(
reference=f"{transaction_id}-T",
date=transaction_date,
description=f"Taxa Paypal - {transaction_description}",
amount=transaction_fee_amount,
bank_account=bank_account,
category=bank_fee_category,
source="paypal-fetch-transactions",
created_by=user,
)
)

return results
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from django.contrib.auth import get_user_model

from thebook.bookkeeping.models import BankAccount, Category, Transaction
from thebook.webhooks.paypal.services import fetch_transactions
from thebook.integrations.paypal.services import fetch_transactions

SAMPLE_PAYLOADS_DIR = Path(__file__).parent / "sample_payloads"

Expand Down
8 changes: 4 additions & 4 deletions thebook/members/templates/emails/onboarding.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,30 @@ Boas vindas ao **Laboratório Hacker de Campinas**. Agora você é oficialmente

# Código de conduta

Todas as pessoas que frequentam o LHC (inclusive nos espaços virtuais) devem seguir o nosso Código de Conduta. E você deve conhecê-lo, segui-lo e também, como uma nova pessoa associada, ter a responsabilidade de garantir que ele seja cumprido em todos os nosso espaços pelas outras pessoas. Você pode ler a versão atualizada em:
Todas as pessoas que frequentam o LHC (inclusive nos espaços virtuais) devem seguir o nosso Código de Conduta. E você deve conhecê-lo, segui-lo e também, como uma nova pessoa associada, ter a responsabilidade de garantir que ele seja cumprido em todos os nossos espaços pelas outras pessoas. Você pode ler a versão atualizada em:

https://github.com/lhc/estatuto/blob/master/src/codigo_conduta.md

# Contribuição Associativa

Ao associar-se ao LHC você deve iniciar o pagamento de sua contribuição associativa. Temos dois valores de contribuição: R$85 e R$110. Não existe nenhuma diferença em direitos do uso do espaço ao escolher qualquer valor. Apenas damos mais uma opção para que quem puder, contribua com um valor maior.

Para iniciar o pagamento, escolha o link abaixo e faça sua assinatura. O valor será cobrado no seu cartão de crédito mensalmente no dia do mês que você fez o primeiro pagamento.
Para iniciar o pagamento, escolha o link abaixo e faça sua assinatura. O valor será cobrado no seu cartão de crédito mensalmente no dia do mês em que você fez o primeiro pagamento.

Mensalmente - R$85 - via PayPal
https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MTSKUDJM463MY

Mensalmente - R$110 - via PayPal
https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4WXUZR98BEF22

> Estamos testando a opção de Pix Automático, onde o LHC inicia uma cobrança que será debitida da conta via Pix mensalmente
> Estamos testando a opção de Pix Automático, onde o LHC inicia uma cobrança que será debitada da conta via Pix mensalmente
> em um data pré-determinada. Caso queira usar essa opção, mande uma mensagem para contato@lhc.net.br para solicitar
> mais informações. Porém por ser algo novo, alguns bancos não implementam corretamente o protocolo (até o momento sabemos que o
> banco XP não funciona), então nem todas as contas podem ser cadastradas.

# Estatuto e Regimento Interno

O LHC é uma associação sem fins lucrativos. Desta maneira temos registrado um Estatuto e um Regimento Interno que definem todas as regras e burocracias necessárias para o funcionamento da Associação. Muito importante conhecer estes documentos.
O LHC é uma associação sem fins lucrativos. Desta maneira temos registrado um Estatuto e um Regimento Interno que definem todas as regras e burocracias necessárias para o funcionamento da Associação. É muito importante conhecer estes documentos.

Você pode acessá-los em:

Expand Down
2 changes: 1 addition & 1 deletion thebook/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
SECRET_KEY = config("SECRET_KEY")

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True # config("DEBUG", default=False, cast=bool)
DEBUG = config("DEBUG", default=False, cast=bool)


LOGGING = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.core.management.base import BaseCommand

from thebook.webhooks.paypal.services import fetch_transactions
from thebook.integrations.paypal.services import fetch_transactions


class Command(BaseCommand):
Expand Down
123 changes: 0 additions & 123 deletions thebook/webhooks/paypal/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,129 +39,6 @@ def _get_subscription(billing_agreement_id, access_token=None):
return subscription


def fetch_transactions(start_date: datetime.date, end_date: datetime.date):
results = []

bank_account, _ = BankAccount.objects.get_or_create(
name=settings.PAYPAL_BANK_ACCOUNT
)
bank_fee_category, _ = Category.objects.get_or_create(
name=settings.BANK_FEE_CATEGORY_NAME
)
bank_account_transfer_category, _ = Category.objects.get_or_create(
name="Transferência entre contas bancárias"
)

user = get_user_model().objects.get_or_create_automation_user()
access_token = _get_paypal_access_token()

params = {
"start_date": start_date.strftime("%Y-%m-%dT00:00:00-00:00"),
"end_date": end_date.strftime("%Y-%m-%dT23:59:59-00:00"),
}
url = f"{settings.PAYPAL_API_BASE_URL}/v1/reporting/transactions"
response = requests.get(
url,
headers={"Authorization": f"Bearer {access_token}"},
params=params,
)
data = response.json()

for transaction in jmespath.search("transaction_details", data) or []:
transaction_status = jmespath.search(
"transaction_info.transaction_status", transaction
)
if transaction_status != "S":
# https://developer.paypal.com/docs/api/transaction-search/v1/#search_get!in=query&path=transaction_status&t=request
continue

transaction_id = jmespath.search("transaction_info.transaction_id", transaction)
if Transaction.objects.filter(reference=transaction_id).exists():
continue

transaction_currency_code = jmespath.search(
"transaction_info.transaction_amount.currency_code", transaction
)
if transaction_currency_code == "USD":
# TODO - Process USD transactions
continue
transaction_amount = decimal.Decimal(
jmespath.search("transaction_info.transaction_amount.value", transaction)
)

raw_date = jmespath.search(
"transaction_info.transaction_initiation_date", transaction
)
transaction_date = datetime.datetime.strptime(
raw_date, "%Y-%m-%dT%H:%M:%SZ"
).date()
transaction_fee_amount = decimal.Decimal(
jmespath.search("transaction_info.fee_amount.value", transaction) or "0"
)

transaction_type = jmespath.search(
"transaction_info.transaction_event_code", transaction
)
is_bank_account_transfer = transaction_type in ("T0400", "T0403")
if is_bank_account_transfer:
transaction_category = bank_account_transfer_category
transaction_description = (
f"Transferência entre contas bancárias - {transaction_id}"
)
else:
transaction_category = None
transaction_description = jmespath.search(
"transaction_info.transaction_subject", transaction
)

if transaction_type == "T0002":
# This flow only applies to Subscription payment
# https://developer.paypal.com/docs/transaction-search/transaction-event-codes/
paypal_reference_id = jmespath.search(
"transaction_info.paypal_reference_id", transaction
)
subscription = _get_subscription(paypal_reference_id, access_token)
given_name = (
jmespath.search("subscriber.name.given_name", subscription) or ""
)
surname = jmespath.search("subscriber.name.surname", subscription) or ""
full_name = " ".join([given_name, surname]).strip()
payer_id = jmespath.search("subscriber.payer_id", subscription) or ""
description_parts = (full_name, payer_id)
transaction_description = " - ".join(
[part for part in description_parts if part]
)

results.append(
Transaction(
reference=transaction_id,
date=transaction_date,
description=transaction_description,
amount=transaction_amount,
bank_account=bank_account,
category=transaction_category,
source="paypal-fetch-transactions",
created_by=user,
)
)

if not is_bank_account_transfer:
results.append(
Transaction(
reference=f"{transaction_id}-T",
date=transaction_date,
description=f"Taxa Paypal - {transaction_description}",
amount=transaction_fee_amount,
bank_account=bank_account,
category=bank_fee_category,
source="paypal-fetch-transactions",
created_by=user,
)
)

return results


def _extract_amount(payload):
currency = jmespath.search("resource.amount.currency", payload)

Expand Down