Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge develop (Sprint 6) #61

Merged
merged 30 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
64e00e9
fix: restrict category endpoints (+others)
MegaRedHand Jun 1, 2024
40f9fd0
feat: add transactions and balances
MegaRedHand Jun 2, 2024
f96612e
refactor: merge balances and users_to_group table
MegaRedHand Jun 2, 2024
58b0b3d
chore: run linter and fix errors
MegaRedHand Jun 2, 2024
aa4720e
feat: accept emails in add_user_to_group
MegaRedHand Jun 2, 2024
00bc942
Remove redundant code as per CR
MegaRedHand Jun 4, 2024
cd1c59a
fix: fetch user from DB instead of jwt
MegaRedHand Jun 4, 2024
79a2d77
refactor: use isinstance instead of type
MegaRedHand Jun 4, 2024
f3fffc7
Merge pull request #51 from VaquitApp/balances
MegaRedHand Jun 4, 2024
3a8dbb0
chore: change psycopg2 to psycopg2-binary
MegaRedHand Jun 9, 2024
287779d
feat: add payment endpoints
MegaRedHand Jun 9, 2024
83aeedc
test: add unit tests
MegaRedHand Jun 9, 2024
701ae1a
chore: remove transactions from model
MegaRedHand Jun 9, 2024
12dcce8
WIP: falta testeo local y creacion de tests
danielaojeda1 Jun 10, 2024
a632877
Solicitar recordatorio de pago listo, solo falta make test
danielaojeda1 Jun 11, 2024
71c9d92
Fix: tests and email templates
gabokatta Jun 12, 2024
b3ff92a
Tests and email tinkering
gabokatta Jun 12, 2024
6024d16
Final touches
gabokatta Jun 12, 2024
145a1d8
Update MailDependency type
MegaRedHand Jun 12, 2024
84cc4ae
typing for send_reminder
gabokatta Jun 12, 2024
5044d0b
Merge pull request #59 from VaquitApp/feature/request_payment_reminder
MegaRedHand Jun 12, 2024
9bacd8f
Merge branch 'develop' into feature/Registrar-Pago-Personal
MegaRedHand Jun 12, 2024
3f5deab
Merge pull request #57 from VaquitApp/feature/Registrar-Pago-Personal
ovr4ulin Jun 12, 2024
587d5d1
Add unique spendings, installment spendings and recurring spendings
ovr4ulin Jun 12, 2024
3ff52a5
Merge branch 'develop' into feature/add-installment-spendings
ovr4ulin Jun 12, 2024
d7d6bee
fix create_installment_spending
ovr4ulin Jun 12, 2024
af7d6c0
fix some bugs
ovr4ulin Jun 12, 2024
a287092
Remove an unused class
ovr4ulin Jun 12, 2024
2584531
Merge pull request #60 from VaquitApp/feature/add-installment-spendings
ovr4ulin Jun 12, 2024
0ba4662
fix: update tests with new spending-related endpoints
MegaRedHand Jun 12, 2024
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
2 changes: 2 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[flake8]
max-line-length = 88
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,4 @@ pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python

*.db
.vscode
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ lint:

test:
rm test.db 2> /dev/null || true
DB_NAME="./test.db" poetry run pytest -svv .
DB_NAME="./test.db" poetry run pytest -svvx --ff .

CONTAINER_NAME="postgres_test_db"

Expand Down
667 changes: 370 additions & 297 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ fastapi = "^0.111.0"
sqlalchemy = "^2.0.30"
uvicorn = "^0.29.0"
flake8 = "^7.0.0"
psycopg2 = "^2.9.9"
psycopg2-binary = "^2.9.9"
python-jose = {extras = ["cryptography"], version = "^3.3.0"}
sib-api-v3-sdk = "^7.6.0"

Expand Down
192 changes: 178 additions & 14 deletions src/crud.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from sqlalchemy import delete, select
from typing import List, Optional
from sqlalchemy import select
from sqlalchemy.orm import Session
from uuid import UUID

Expand All @@ -14,7 +15,7 @@ def get_user_by_id(db: Session, id: int):
return db.query(models.User).filter(models.User.id == id).first()


def get_user_by_email(db: Session, email: str) -> models.User:
def get_user_by_email(db: Session, email: str) -> Optional[models.User]:
return db.query(models.User).filter(models.User.email == email).first()


Expand Down Expand Up @@ -87,8 +88,8 @@ def create_group(db: Session, group: schemas.GroupCreate, user_id: int):

# Add the owner to the group members
db_user = get_user_by_id(db, user_id)
db_user.groups.add(db_group)

db_group.members.add(db_user)
db.commit()
db.refresh(db_group)
return db_group
Expand Down Expand Up @@ -125,45 +126,135 @@ def update_group_status(db: Session, group: models.Group, status: bool):
return group


def add_user_to_group(db: Session, group: models.Group, user_id: int):
user = get_user_by_id(db, user_id)
def add_user_to_group(db: Session, user: models.User, group: models.Group):
group.members.add(user)
db.commit()
db.refresh(group)
return group


################################################
# SPENDINGS
# ALL SPENDINGS
################################################

def get_all_spendings_by_group_id(db: Session, group_id: int):

unique_spendings = db.query(models.UniqueSpending).filter(models.UniqueSpending.group_id == group_id).limit(100).all()
installment_spendings = db.query(models.InstallmentSpending).filter(models.InstallmentSpending.group_id == group_id).limit(100).all()
recurring_spendings = db.query(models.RecurringSpending).filter(models.RecurringSpending.group_id == group_id).limit(100).all()

def create_spending(db: Session, spending: schemas.SpendingCreate, user_id: int):
db_spending = models.Spending(owner_id=user_id, **dict(spending))
unique_spendings = list(map(lambda spending: {**spending.__dict__, "type": "unique_spending"}, unique_spendings))
installment_spendings = list(map(lambda spending: {**spending.__dict__, "type": "installment_spending"}, installment_spendings))
recurring_spendings = list(map(lambda spending: {**spending.__dict__, "type": "recurring_spending"}, recurring_spendings))

return unique_spendings + installment_spendings + recurring_spendings

################################################
# UNIQUE SPENDINGS
################################################

def create_unique_spending(db: Session, spending: schemas.UniqueSpendingCreate, user_id: int):
db_spending = models.UniqueSpending(owner_id=user_id, **dict(spending))
db.add(db_spending)
db.commit()
db.refresh(db_spending)
update_balances_from_spending(db, db_spending)
db.refresh(db_spending)
return db_spending


def get_spendings_by_group_id(db: Session, group_id: int):
def get_unique_spendings_by_group_id(db: Session, group_id: int):
return (
db.query(models.Spending)
.filter(models.Spending.group_id == group_id)
db.query(models.UniqueSpending)
.filter(models.UniqueSpending.group_id == group_id)
.limit(100)
.all()
)


def get_spendings_by_category(db: Session, category_id: int):
################################################
# INSTALLMENT SPENDINGS
################################################


def create_installment_spending(db: Session, spending: schemas.InstallmentSpendingCreate, user_id: int, current_installment:int):
db_spending = models.InstallmentSpending(owner_id=user_id, current_installment=current_installment,**dict(spending))
db.add(db_spending)
db.commit()
db.refresh(db_spending)
update_balances_from_spending(db, db_spending)
db.refresh(db_spending)
return db_spending


def get_installment_spendings_by_group_id(db: Session, group_id: int):
return (
db.query(models.Spending)
.filter(models.Spending.category_id == category_id)
db.query(models.InstallmentSpending)
.filter(models.InstallmentSpending.group_id == group_id)
.limit(100)
.all()
)


################################################
# RECURRING SPENDINGS
################################################


def create_recurring_spending(db: Session, spending: schemas.RecurringSpendingBase, user_id: int):
db_spending = models.RecurringSpending(owner_id=user_id, **dict(spending))
db.add(db_spending)
db.commit()
db.refresh(db_spending)
update_balances_from_spending(db, db_spending)
db.refresh(db_spending)
return db_spending


def get_recurring_spendings_by_id(db: Session, recurring_spendig_id: int):
return db.query(models.RecurringSpending).filter(models.RecurringSpending.id == recurring_spendig_id).first()


def get_recurring_spendings_by_group_id(db: Session, group_id: int):
return (
db.query(models.RecurringSpending)
.filter(models.RecurringSpending.group_id == group_id)
.limit(100)
.all()
)


def put_recurring_spendings(db: Session, db_recurring_spending: models.RecurringSpending, put_recurring_spending: schemas.RecurringSpendingPut):
db_recurring_spending.amount = put_recurring_spending.amount
db_recurring_spending.description = put_recurring_spending.description
db_recurring_spending.category_id = put_recurring_spending.categiry_id
db.commit()
db.refresh(db_recurring_spending)
return db_recurring_spending


################################################
# PAYMENTS
################################################


def create_payment(db: Session, payment: schemas.PaymentCreate):
db_payment = models.Payment(**dict(payment))
update_balances_from_payment(db, db_payment)
db.add(db_payment)
db.commit()
db.refresh(db_payment)
return db_payment


def get_payments_by_group_id(db: Session, group_id: int):
return (
db.query(models.Payment)
.filter(models.Payment.group_id == group_id)
.limit(100)
.all()
)

################################################
# BUDGETS
################################################
Expand Down Expand Up @@ -233,3 +324,76 @@ def update_invite_status(
db.commit()
db.refresh(db_invite)
return db_invite


################################################
# REMINDERS
################################################


def create_payment_reminder(
db: Session, payment_reminder: schemas.PaymentReminderCreate, sender_id: int
):
db_reminder = models.PaymentReminder(
sender_id=sender_id,
receiver_id=payment_reminder.receiver_id,
group_id=payment_reminder.group_id,
)

if payment_reminder.message is not None:
db_reminder.message = payment_reminder.message

db.add(db_reminder)
db.commit()
db.refresh(db_reminder)
return db_reminder


################################################
# BALANCES
################################################


def update_balances_from_spending(db: Session, spending: models.UniqueSpending):
group = get_group_by_id(db, spending.group_id)
balances = sorted(
get_balances_by_group_id(db, spending.group_id), key=lambda x: x.user_id
)
members = sorted(group.members, key=lambda x: x.id)
# TODO: implement division strategy
# TODO: this truncates decimals
amount_per_member = spending.amount // len(members)
for user, balance in zip(members, balances):
amount = -amount_per_member

if spending.owner_id == user.id:
amount += spending.amount

balance.current_balance += amount

db.commit()


def update_balances_from_payment(db: Session, payment: models.Payment):
balances = get_balances_by_group_id(db, payment.group_id)

# Update payer balance
payer = get_user_by_id(db, payment.from_id)
payer_balance = next(filter(lambda x: x.user_id == payer.id, balances))
payer_balance.current_balance += payment.amount

# Update payee balance
payee = get_user_by_id(db, payment.to_id)
payee_balance = next(filter(lambda x: x.user_id == payee.id, balances))
payee_balance.current_balance -= payment.amount

db.commit()


def get_balances_by_group_id(db: Session, group_id: int) -> List[models.Balance]:
return (
db.query(models.Balance)
.filter(models.Balance.group_id == group_id)
.limit(100)
.all()
)
53 changes: 48 additions & 5 deletions src/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,34 @@
from datetime import datetime
import os
from logging import info, error, warning
from typing import Optional
import sib_api_v3_sdk as sdk
from sib_api_v3_sdk.rest import ApiException

from src import schemas

BASE_URL = os.environ.get("BASE_URL", "http://localhost:3000")
API_KEY = os.environ.get("EMAIL_API_KEY")
TEMPLATE_ID = 1

INVITE_TEMPLATE_ID = 1
REMINDER_TEMPLATE_ID = 2
DEFAULT_REMINDER = "No demores muucho con la deuda! :D"


class MailSender(ABC):
@abstractmethod
def send(self, sender: str, receiver: str, group_name: str) -> bool:
def send_invite(self, sender: str, receiver: str, group_name: str) -> bool:
pass

@abstractmethod
def send_reminder(
self, sender: str, receiver: str, group_id: int, message: Optional[str]
) -> bool:
pass


class ProdMailSender(MailSender):
def send(
def send_invite(
self, sender: str, receiver: str, group: schemas.Group, token: str
) -> bool:
configuration = sdk.Configuration()
Expand All @@ -35,7 +45,35 @@ def send(
"join_link": f"{BASE_URL}/invites/accept/{token}",
}

email = sdk.SendSmtpEmail(to=to, template_id=TEMPLATE_ID, params=params)
email = sdk.SendSmtpEmail(to=to, template_id=INVITE_TEMPLATE_ID, params=params)

try:
response = api_instance.send_transac_email(email)
info(response)
return True
except ApiException as e:
error(f"Failed to send email with error: {e}")
return False

def send_reminder(
self, sender: str, receiver: str, group: schemas.Group, message: Optional[str]
) -> bool:
configuration = sdk.Configuration()
configuration.api_key["api-key"] = API_KEY

api_instance = sdk.TransactionalEmailsApi(sdk.ApiClient(configuration))

to = [{"email": receiver}]
params = {
"sender": sender,
"message": DEFAULT_REMINDER if message is None else message,
"landing_page": f"{BASE_URL}",
"group_name": group.name,
}

email = sdk.SendSmtpEmail(
to=to, template_id=REMINDER_TEMPLATE_ID, params=params
)

try:
response = api_instance.send_transac_email(email)
Expand All @@ -47,11 +85,16 @@ def send(


class LocalMailSender(MailSender):
def send(
def send_invite(
self, sender: str, receiver: str, group: schemas.Group, token: str
) -> bool:
return True

def send_reminder(
self, sender: str, receiver: str, group: schemas.Group, message: Optional[str]
) -> bool:
return True


if API_KEY is not None:
mail_service = ProdMailSender()
Expand Down
Loading
Loading