diff --git a/backend/trip/alembic/versions/16bffb744f33_trip_notes.py b/backend/trip/alembic/versions/16bffb744f33_trip_notes.py new file mode 100644 index 0000000..440d83b --- /dev/null +++ b/backend/trip/alembic/versions/16bffb744f33_trip_notes.py @@ -0,0 +1,33 @@ +"""Trip Notes + +Revision ID: 16bffb744f33 +Revises: 8e12410a0b8e +Create Date: 2025-10-04 16:15:08.434221 + +""" + +import sqlalchemy as sa +import sqlmodel.sql.sqltypes +from alembic import op + +# revision identifiers, used by Alembic. +revision = "16bffb744f33" +down_revision = "8e12410a0b8e" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("trip", schema=None) as batch_op: + batch_op.add_column(sa.Column("notes", sqlmodel.sql.sqltypes.AutoString(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("trip", schema=None) as batch_op: + batch_op.drop_column("notes") + + # ### end Alembic commands ### diff --git a/backend/trip/alembic/versions/23320f01d8ce_tripitem_paid_by.py b/backend/trip/alembic/versions/23320f01d8ce_tripitem_paid_by.py new file mode 100644 index 0000000..65e8e3c --- /dev/null +++ b/backend/trip/alembic/versions/23320f01d8ce_tripitem_paid_by.py @@ -0,0 +1,37 @@ +"""TripItem paid_by + +Revision ID: 23320f01d8ce +Revises: 16bffb744f33 +Create Date: 2025-10-04 16:22:33.968337 + +""" + +import sqlalchemy as sa +import sqlmodel.sql.sqltypes +from alembic import op + +# revision identifiers, used by Alembic. +revision = "23320f01d8ce" +down_revision = "16bffb744f33" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("tripitem", schema=None) as batch_op: + batch_op.add_column(sa.Column("paid_by", sa.Integer(), nullable=True)) + batch_op.create_foreign_key( + batch_op.f("fk_tripitem_paid_by_user"), "user", ["paid_by"], ["username"], ondelete="SET NULL" + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("tripitem", schema=None) as batch_op: + batch_op.drop_constraint(batch_op.f("fk_tripitem_paid_by_user"), type_="foreignkey") + batch_op.drop_column("paid_by") + + # ### end Alembic commands ### diff --git a/backend/trip/models/models.py b/backend/trip/models/models.py index 4892b80..f1e09ce 100644 --- a/backend/trip/models/models.py +++ b/backend/trip/models/models.py @@ -251,6 +251,7 @@ class TripBase(SQLModel): name: str archived: bool | None = None currency: str | None = settings.DEFAULT_CURRENCY + notes: str | None = None class Trip(TripBase, table=True): @@ -259,8 +260,12 @@ class Trip(TripBase, table=True): image: Image | None = Relationship(back_populates="trips") user: str = Field(foreign_key="user.username", ondelete="CASCADE") - places: list["Place"] = Relationship(back_populates="trips", sa_relationship_kwargs={"order_by": "Place.name"}, link_model=TripPlaceLink) - days: list["TripDay"] = Relationship(back_populates="trip", sa_relationship_kwargs={"order_by": "TripDay.label"}, cascade_delete=True) + places: list["Place"] = Relationship( + back_populates="trips", sa_relationship_kwargs={"order_by": "Place.name"}, link_model=TripPlaceLink + ) + days: list["TripDay"] = Relationship( + back_populates="trip", sa_relationship_kwargs={"order_by": "TripDay.label"}, cascade_delete=True + ) shares: list["TripShare"] = Relationship(back_populates="trip", cascade_delete=True) packing_items: list["TripPackingListItem"] = Relationship(back_populates="trip", cascade_delete=True) checklist_items: list["TripChecklistItem"] = Relationship(back_populates="trip", cascade_delete=True) @@ -321,6 +326,7 @@ class TripRead(TripBase): collaborators=[TripMemberRead.serialize(m) for m in obj.memberships], shared=bool(obj.shares), currency=obj.currency if obj.currency else settings.DEFAULT_CURRENCY, + notes=obj.notes, ) @@ -414,11 +420,14 @@ class TripItem(TripItemBase, table=True): day_id: int = Field(foreign_key="tripday.id", ondelete="CASCADE") day: TripDay | None = Relationship(back_populates="items") + paid_by: int | None = Field(default=None, foreign_key="user.username", ondelete="SET NULL") + class TripItemCreate(TripItemBase): place: int | None = None status: TripItemStatusEnum | None = None image: str | None = None + paid_by: str | None = None class TripItemUpdate(TripItemBase): @@ -428,6 +437,7 @@ class TripItemUpdate(TripItemBase): day_id: int | None = None status: TripItemStatusEnum | None = None image: str | None = None + paid_by: str | None = None class TripItemRead(TripItemBase): @@ -437,6 +447,7 @@ class TripItemRead(TripItemBase): status: TripItemStatusEnum | None image: str | None image_id: int | None + paid_by: str | None @classmethod def serialize(cls, obj: TripItem) -> "TripItemRead": @@ -454,6 +465,7 @@ class TripItemRead(TripItemBase): image=_prefix_assets_url(obj.image.filename) if obj.image else None, image_id=obj.image_id, gpx=obj.gpx, + paid_by=obj.paid_by, ) diff --git a/backend/trip/routers/trips.py b/backend/trip/routers/trips.py index 979b2b2..5b530cb 100644 --- a/backend/trip/routers/trips.py +++ b/backend/trip/routers/trips.py @@ -246,6 +246,34 @@ def delete_trip( return {} +@router.get("/{trip_id}/balance") +def get_trip_balance( + session: SessionDep, + trip_id: int, + current_user: Annotated[str, Depends(get_current_username)], +): + _verify_trip_member(session, trip_id, current_user) + + members = _trip_usernames(session, trip_id) + if len(members) < 2: + raise HTTPException(status_code=400, detail="Bad request") + + trip_items = session.exec( + select(TripItem) + .join(TripDay) + .where(TripDay.trip_id == trip_id, TripItem.price.is_not(None), TripItem.paid_by.is_not(None)) + ).all() + + paid_by_map = {m: 0 for m in members} + for item in trip_items: + if not item.price or not item.paid_by: + continue + paid_by_map[item.paid_by] += item.price + xpected_per_person = sum(paid_by_map.values()) / len(members) + + return {member: paid_by_map[member] - xpected_per_person for member in paid_by_map} + + @router.post("/{trip_id}/days", response_model=TripDayRead) def create_tripday( td: TripDayBase, @@ -364,6 +392,14 @@ def create_tripitem( raise HTTPException(status_code=400, detail="Bad request") new_item.place_id = item.place + if item.paid_by: + if db_trip.user != item.paid_by: + is_member = item.paid_by in _trip_usernames(session, trip_id) + if not is_member: + raise HTTPException(status_code=400, detail="User is not a trip member") + + new_item.paid_by = item.paid_by + session.add(new_item) session.commit() session.refresh(new_item) @@ -442,6 +478,17 @@ def update_tripitem( if not place_in_trip: raise HTTPException(status_code=400, detail="Bad request") + if "paid_by" in item_data: + paid_by = item_data.pop("paid_by") + if paid_by: + if paid_by != db_trip.user: + is_member = item.paid_by in _trip_usernames(session, trip_id) + if not is_member: + raise HTTPException(status_code=400, detail="User is not a trip member") + db_item.paid_by = paid_by + else: + db_item.paid_by = None + for key, value in item_data.items(): setattr(db_item, key, value) @@ -756,7 +803,6 @@ def invite_trip_member( raise HTTPException(status_code=409, detail="The resource already exists") db_user = session.get(User, data.user) - print(data.user, db_user) if not db_user: raise HTTPException(status_code=404, detail="Not found") @@ -774,7 +820,6 @@ def delete_trip_member( username: str, current_user: Annotated[str, Depends(get_current_username)], ): - print("yo") db_trip = _get_trip_or_404(session, trip_id) _verify_trip_member(session, trip_id, current_user) @@ -793,6 +838,14 @@ def delete_trip_member( if not member: raise HTTPException(status_code=404, detail="Not found") + # Set NULL to TripItem.paid_by for this username + trip_items = session.exec( + select(TripItem).join(TripDay).where(TripDay.trip_id == trip_id, TripItem.paid_by == username) + ).all() + for item in trip_items: + item.paid_by = None + session.add_all(trip_items) + session.delete(member) session.commit() return {} diff --git a/src/src/app/components/trip/trip.component.html b/src/src/app/components/trip/trip.component.html index bb27f6f..eee5def 100644 --- a/src/src/app/components/trip/trip.component.html +++ b/src/src/app/components/trip/trip.component.html @@ -57,7 +57,7 @@