Skip to content

Commit

Permalink
Merge pull request #59 from VaquitApp/feature/request_payment_reminder
Browse files Browse the repository at this point in the history
Solicitar Recordatorio de Pago [2]
  • Loading branch information
MegaRedHand authored Jun 12, 2024
2 parents f3fffc7 + 84cc4ae commit 5044d0b
Show file tree
Hide file tree
Showing 6 changed files with 219 additions and 8 deletions.
16 changes: 16 additions & 0 deletions src/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,22 @@ def update_invite_status(
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

################################################
# TRANSACTIONS
Expand Down
46 changes: 41 additions & 5 deletions src/mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,31 @@
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 +42,32 @@ 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,10 +79,14 @@ 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:
Expand Down
37 changes: 34 additions & 3 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from fastapi import Depends, FastAPI, HTTPException, Header

from src import crud, models, schemas, auth
from src.mail import mail_service, is_expired_invite
from src.mail import MailSender, mail_service, is_expired_invite
from src.database import SessionLocal, engine
from sqlalchemy.orm import Session

Expand Down Expand Up @@ -45,7 +45,7 @@ def ensure_user(db: DbDependency, x_user: Annotated[str, Header()]) -> models.Us
app = FastAPI(dependencies=[Depends(get_db)])

UserDependency = Annotated[models.User, Depends(ensure_user)]
MailDependency = Annotated[models.Invite, Depends(get_mail_sender)]
MailDependency = Annotated[MailSender, Depends(get_mail_sender)]

################################################
# USERS
Expand Down Expand Up @@ -429,7 +429,7 @@ def send_invite(
)

token = uuid4()
sent_ok = mail.send(
sent_ok = mail.send_invite(
sender=user.email, receiver=receiver.email, group=target_group, token=token.hex
)

Expand Down Expand Up @@ -479,3 +479,34 @@ def accept_invite(db: DbDependency, user: UserDependency, invite_token: str):

crud.add_user_to_group(db, user, target_group)
return crud.update_invite_status(db, target_invite, schemas.InviteStatus.ACCEPTED)

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

@app.post("/payment_reminder", status_code=HTTPStatus.CREATED)
def send_payment_reminder(db: DbDependency,
user: UserDependency,
mail: MailDependency,
payment_reminder: schemas.PaymentReminderCreate):

receiver = crud.get_user_by_email(db, payment_reminder.receiver_email)
if receiver is None:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="No se encontro el usuario receptor."
)
group = crud.get_group_by_id(db, payment_reminder.group_id)
check_group_exists_and_user_is_member(receiver.id, group)
check_group_is_unarchived(group)
payment_reminder.receiver_id = receiver.id


sent_ok = mail.send_reminder(
sender=user.email, receiver=receiver.email, group=group, message=payment_reminder.message)

if sent_ok:
return crud.create_payment_reminder(db, payment_reminder, user.id)
else:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail="No se pudo enviar recordatorio de pago al usuario."
)
11 changes: 11 additions & 0 deletions src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,14 @@ class Balance(Base):
current_balance = Column(Integer, default=0)

__table_args__ = (UniqueConstraint("user_id", "group_id"),)

class PaymentReminder(Base):
__tablename__ = "payment_reminders"

id = Column(Integer, primary_key=True, index=True)
sender_id = Column(Integer, ForeignKey("users.id"))
receiver_id = Column(Integer, ForeignKey("users.id"))
group_id = Column(Integer, ForeignKey("groups.id"))
message = Column(String)
creation_date: Mapped[datetime] = mapped_column(DateTime, default=func.now())

17 changes: 17 additions & 0 deletions src/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,20 @@ class Invite(InviteBase):
id: int
sender_id: int
status: InviteStatus

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

class PaymentReminderBase(BaseModel):
creation_date: Optional[datetime] = Field(None)
receiver_id: Optional[int] = Field(None)
group_id: int
message: Optional[str] = Field(None)

class PaymentReminderCreate(PaymentReminderBase):
receiver_email: str

class PaymentReminder(PaymentReminderBase):
id: int
sender_id: int
100 changes: 100 additions & 0 deletions src/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ def make_user_credentials(client: TestClient, email: str):
assert response.status_code == HTTPStatus.CREATED
return schemas.UserCredentials(**response.json())

def add_user_to_group(client: TestClient, group_id: int, new_member_id: int, credentials: schemas.UserCredentials):
response = client.post(
url=f"/group/{group_id}/member",
headers={"x-user": credentials.jwt},
json={"user_identifier": new_member_id},
)
assert response.status_code == HTTPStatus.CREATED


@pytest.fixture()
def some_credentials(client: TestClient) -> schemas.UserCredentials:
Expand Down Expand Up @@ -926,3 +934,95 @@ def test_balance_multiple_members(
some_spending.amount if user.id == some_spending.owner_id else 0
)
assert balance["current_balance"] == expected_balance

################################################
# PAYMENT REMINDERS
################################################
@pytest.fixture
def some_payment_reminder(
client: TestClient,
some_credentials: schemas.UserCredentials,
some_other_credentials: schemas.UserCredentials,
some_group: schemas.Group,
):
add_user_to_group(client, some_group.id, some_other_credentials.id, some_credentials)

# Create PaymentReminder
response = client.post(
url="/payment_reminder",
json={
"receiver_email": some_other_credentials.email,
"group_id": some_group.id,
},
headers={"x-user": some_credentials.jwt},
)
assert response.status_code == HTTPStatus.CREATED
response_body = response.json()

assert "creation_date" in response_body
assert response_body["group_id"] == some_group.id
assert response_body["sender_id"] == some_credentials.id
assert response_body["receiver_id"] == some_other_credentials.id

return schemas.PaymentReminder(**response_body)

def test_send_reminder(client: TestClient, some_payment_reminder: schemas.PaymentReminder):
# NOTE: test is inside fixture
pass


def test_send_payment_reminder_to_non_registered_user(
client: TestClient,
some_credentials: schemas.UserCredentials,
some_group: schemas.Group
):
response = client.post(
url="/payment_reminder",
json={
"receiver_email": "pepe@gmail.com",
"group_id": some_group.id,
},
headers={"x-user": some_credentials.jwt},
)
assert response.status_code == HTTPStatus.NOT_FOUND

def test_send_payment_reminder_on_non_existant_group(
client: TestClient, some_credentials: schemas.UserCredentials
):
response = client.post(
url="/payment_reminder",
json={"receiver_email": some_credentials.email, "group_id": 12345},
headers={"x-user": some_credentials.jwt},
)
assert response.status_code == HTTPStatus.NOT_FOUND

def test_send_reminder_to_non_member(client: TestClient, some_credentials: schemas.UserCredentials, some_group: schemas.Group):

new_user = make_user_credentials(client, "pepitoelmascapo@gmail.com")

response = client.post(
url="/payment_reminder",
json={"receiver_email": new_user.email, "group_id": some_group.id},
headers={"x-user": some_credentials.jwt},
)
assert response.status_code == HTTPStatus.NOT_FOUND

def test_send_reminder_to_archived_group(client: TestClient,
some_credentials: schemas.UserCredentials,
some_other_credentials: schemas.UserCredentials,
some_group: schemas.Group):

add_user_to_group(client, some_group.id, some_other_credentials.id, some_credentials)

response = client.put(
url=f"/group/{some_group.id}/archive", headers={"x-user": some_credentials.jwt}
)
assert response.status_code == HTTPStatus.OK


response = client.post(
url="/payment_reminder",
json={"receiver_email": some_other_credentials.email, "group_id": some_group.id},
headers={"x-user": some_credentials.jwt},
)
assert response.status_code == HTTPStatus.NOT_ACCEPTABLE

0 comments on commit 5044d0b

Please sign in to comment.