diff --git a/.DS_Store b/.DS_Store index 3b30378..a6f36b8 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index ad67955..433bcf7 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,10 @@ target # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# SQLite database +.env +*.db +*.db-shm +*.db-wal +.DS_Store \ No newline at end of file diff --git a/migration/src/lib.rs b/migration/src/lib.rs index 6a7d5aa..40943ee 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -1,6 +1,6 @@ pub use sea_orm_migration::prelude::*; -mod m20260515_195629_create_users_table; +mod m20260529_144919_create_settlemate_tables; pub struct Migrator; @@ -8,7 +8,7 @@ pub struct Migrator; impl MigratorTrait for Migrator { fn migrations() -> Vec> { vec![ - Box::new(m20260515_195629_create_users_table::Migration), + Box::new(m20260529_144919_create_settlemate_tables::Migration), ] } } diff --git a/migration/src/m20260515_195629_create_users_table.rs b/migration/src/m20260515_195629_create_users_table.rs deleted file mode 100644 index bbfe665..0000000 --- a/migration/src/m20260515_195629_create_users_table.rs +++ /dev/null @@ -1,50 +0,0 @@ -use sea_orm_migration::{prelude::*, schema::*}; - -#[derive(DeriveMigrationName)] -pub struct Migration; - -#[async_trait::async_trait] -impl MigrationTrait for Migration { - async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { - // Replace the sample below with your own migration scripts - - manager - .create_table( - Table::create() - .table(Users::Table) - .if_not_exists() - .col(pk_auto(Users::Id)) - .col(string(Users::Name).not_null()) - .col(string(Users::Email).not_null().unique_key()) - .col(string(Users::PasswordHash).not_null()) - .col( - timestamp(Users::CreatedAt) - .not_null() - .default(Expr::current_timestamp()), - ) - .to_owned(), - ) - .await - } - - async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { - // Replace the sample below with your own migration scripts - - manager - .drop_table( - Table::drop() - .table(Users::Table) - .to_owned()) - .await - } -} - -#[derive(DeriveIden)] -enum Users { - Table, - Id, - Name, - Email, - PasswordHash, - CreatedAt, -} diff --git a/migration/src/m20260529_144919_create_settlemate_tables.rs b/migration/src/m20260529_144919_create_settlemate_tables.rs new file mode 100644 index 0000000..f3c941b --- /dev/null +++ b/migration/src/m20260529_144919_create_settlemate_tables.rs @@ -0,0 +1,228 @@ +use sea_orm_migration::{prelude::*, schema::*}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(Users::Table) + .if_not_exists() + .col(pk_auto(Users::Id)) + .col(string(Users::Name).not_null()) + .col(string(Users::Email).not_null().unique_key()) + .col(string(Users::PasswordHash).not_null()) + .col( + timestamp(Users::CreatedAt) + .not_null() + .default(Expr::current_timestamp()), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(Groups::Table) + .if_not_exists() + .col(pk_auto(Groups::Id)) + .col(string(Groups::Name).not_null()) + .col( + timestamp(Groups::CreatedAt) + .not_null() + .default(Expr::current_timestamp()), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(GroupMembers::Table) + .if_not_exists() + .col(pk_auto(GroupMembers::Id)) + .col(integer(GroupMembers::GroupId).not_null()) + .col(integer(GroupMembers::UserId).not_null()) + .foreign_key( + ForeignKey::create() + .from(GroupMembers::Table, GroupMembers::GroupId) + .to(Groups::Table, Groups::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .from(GroupMembers::Table, GroupMembers::UserId) + .to(Users::Table, Users::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(Expenses::Table) + .if_not_exists() + .col(pk_auto(Expenses::Id)) + .col(string(Expenses::Description).not_null()) + .col(double(Expenses::Amount).not_null()) + .col(integer(Expenses::PaidBy).not_null()) + .col(integer(Expenses::GroupId)) + .col(string(Expenses::SplitType).not_null()) + .col( + timestamp(Expenses::CreatedAt) + .not_null() + .default(Expr::current_timestamp()), + ) + .foreign_key( + ForeignKey::create() + .from(Expenses::Table, Expenses::PaidBy) + .to(Users::Table, Users::Id), + ) + .foreign_key( + ForeignKey::create() + .from(Expenses::Table, Expenses::GroupId) + .to(Groups::Table, Groups::Id) + .on_delete(ForeignKeyAction::SetNull), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(ExpenseSplits::Table) + .if_not_exists() + .col(pk_auto(ExpenseSplits::Id)) + .col(integer(ExpenseSplits::ExpenseId).not_null()) + .col(integer(ExpenseSplits::UserId).not_null()) + .col(double(ExpenseSplits::Amount)) + .foreign_key( + ForeignKey::create() + .from(ExpenseSplits::Table, ExpenseSplits::ExpenseId) + .to(Expenses::Table, Expenses::Id) + .on_delete(ForeignKeyAction::Cascade), + ) + .foreign_key( + ForeignKey::create() + .from(ExpenseSplits::Table, ExpenseSplits::UserId) + .to(Users::Table, Users::Id), + ) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table(Payments::Table) + .if_not_exists() + .col(pk_auto(Payments::Id)) + .col(integer(Payments::FromId).not_null()) + .col(integer(Payments::ToId).not_null()) + .col(double(Payments::Amount).not_null()) + .col(integer(Payments::GroupId)) + .col( + timestamp(Payments::CreatedAt) + .not_null() + .default(Expr::current_timestamp()), + ) + .foreign_key( + ForeignKey::create() + .from(Payments::Table, Payments::FromId) + .to(Users::Table, Users::Id), + ) + .foreign_key( + ForeignKey::create() + .from(Payments::Table, Payments::ToId) + .to(Users::Table, Users::Id), + ) + .foreign_key( + ForeignKey::create() + .from(Payments::Table, Payments::GroupId) + .to(Groups::Table, Groups::Id) + .on_delete(ForeignKeyAction::SetNull), + ) + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager.drop_table(Table::drop().table(Payments::Table).to_owned()).await?; + manager.drop_table(Table::drop().table(ExpenseSplits::Table).to_owned()).await?; + manager.drop_table(Table::drop().table(Expenses::Table).to_owned()).await?; + manager.drop_table(Table::drop().table(GroupMembers::Table).to_owned()).await?; + manager.drop_table(Table::drop().table(Groups::Table).to_owned()).await?; + manager.drop_table(Table::drop().table(Users::Table).to_owned()).await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum Users { + Table, + Id, + Name, + Email, + PasswordHash, + CreatedAt, +} + +#[derive(DeriveIden)] +enum Groups { + Table, + Id, + Name, + CreatedAt, +} + +#[derive(DeriveIden)] +enum GroupMembers { + Table, + Id, + GroupId, + UserId, +} + +#[derive(DeriveIden)] +enum Expenses { + Table, + Id, + Description, + Amount, + PaidBy, + GroupId, + SplitType, + CreatedAt, +} + +#[derive(DeriveIden)] +enum ExpenseSplits { + Table, + Id, + ExpenseId, + UserId, + Amount, +} + +#[derive(DeriveIden)] +enum Payments { + Table, + Id, + FromId, + ToId, + Amount, + GroupId, + CreatedAt, +} \ No newline at end of file diff --git a/settlemate.db b/settlemate.db deleted file mode 100644 index 9f701a9..0000000 Binary files a/settlemate.db and /dev/null differ diff --git a/src/entities/expense_splits.rs b/src/entities/expense_splits.rs new file mode 100644 index 0000000..5641906 --- /dev/null +++ b/src/entities/expense_splits.rs @@ -0,0 +1,49 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "expense_splits")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub expense_id: i32, + pub user_id: i32, + #[sea_orm(column_type = "Double")] + pub amount: f64, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::expenses::Entity", + from = "Column::ExpenseId", + to = "super::expenses::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Expenses, + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::UserId", + to = "super::users::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Users, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Expenses.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/expenses.rs b/src/entities/expenses.rs new file mode 100644 index 0000000..9a3085a --- /dev/null +++ b/src/entities/expenses.rs @@ -0,0 +1,60 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "expenses")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub description: String, + #[sea_orm(column_type = "Double")] + pub amount: f64, + pub paid_by: i32, + pub group_id: i32, + pub split_type: String, + pub created_at: DateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::expense_splits::Entity")] + ExpenseSplits, + #[sea_orm( + belongs_to = "super::groups::Entity", + from = "Column::GroupId", + to = "super::groups::Column::Id", + on_update = "NoAction", + on_delete = "SetNull" + )] + Groups, + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::PaidBy", + to = "super::users::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Users, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ExpenseSplits.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Groups.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/group_members.rs b/src/entities/group_members.rs new file mode 100644 index 0000000..fc72f7f --- /dev/null +++ b/src/entities/group_members.rs @@ -0,0 +1,47 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "group_members")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub group_id: i32, + pub user_id: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::groups::Entity", + from = "Column::GroupId", + to = "super::groups::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Groups, + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::UserId", + to = "super::users::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Users, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Groups.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Users.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/groups.rs b/src/entities/groups.rs new file mode 100644 index 0000000..5c9dd14 --- /dev/null +++ b/src/entities/groups.rs @@ -0,0 +1,43 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "groups")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub name: String, + pub created_at: DateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::expenses::Entity")] + Expenses, + #[sea_orm(has_many = "super::group_members::Entity")] + GroupMembers, + #[sea_orm(has_many = "super::payments::Entity")] + Payments, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Expenses.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::GroupMembers.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Payments.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/mod.rs b/src/entities/mod.rs index faa69d2..7ad64cc 100644 --- a/src/entities/mod.rs +++ b/src/entities/mod.rs @@ -2,4 +2,9 @@ pub mod prelude; +pub mod expense_splits; +pub mod expenses; +pub mod group_members; +pub mod groups; +pub mod payments; pub mod users; diff --git a/src/entities/payments.rs b/src/entities/payments.rs new file mode 100644 index 0000000..fff44c8 --- /dev/null +++ b/src/entities/payments.rs @@ -0,0 +1,53 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] +#[sea_orm(table_name = "payments")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i32, + pub from_id: i32, + pub to_id: i32, + #[sea_orm(column_type = "Double")] + pub amount: f64, + pub group_id: i32, + pub created_at: DateTimeUtc, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::groups::Entity", + from = "Column::GroupId", + to = "super::groups::Column::Id", + on_update = "NoAction", + on_delete = "SetNull" + )] + Groups, + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::ToId", + to = "super::users::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Users2, + #[sea_orm( + belongs_to = "super::users::Entity", + from = "Column::FromId", + to = "super::users::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + Users1, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Groups.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src/entities/prelude.rs b/src/entities/prelude.rs index 016d44a..aa26e0c 100644 --- a/src/entities/prelude.rs +++ b/src/entities/prelude.rs @@ -1,3 +1,8 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 +pub use super::expense_splits::Entity as ExpenseSplits; +pub use super::expenses::Entity as Expenses; +pub use super::group_members::Entity as GroupMembers; +pub use super::groups::Entity as Groups; +pub use super::payments::Entity as Payments; pub use super::users::Entity as Users; diff --git a/src/entities/users.rs b/src/entities/users.rs index fedd698..d17cf62 100644 --- a/src/entities/users.rs +++ b/src/entities/users.rs @@ -1,8 +1,9 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] #[sea_orm(table_name = "users")] pub struct Model { #[sea_orm(primary_key)] @@ -15,6 +16,31 @@ pub struct Model { } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm(has_many = "super::expense_splits::Entity")] + ExpenseSplits, + #[sea_orm(has_many = "super::expenses::Entity")] + Expenses, + #[sea_orm(has_many = "super::group_members::Entity")] + GroupMembers, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::ExpenseSplits.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Expenses.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::GroupMembers.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/src/services/group_service.rs b/src/services/group_service.rs new file mode 100644 index 0000000..c2d094c --- /dev/null +++ b/src/services/group_service.rs @@ -0,0 +1,97 @@ +use crate::entities::{group_members, groups, users}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, DeleteResult, EntityTrait, IntoActiveModel, + QueryFilter, Set, +}; + +use groups::Entity as Groups; +use group_members::Entity as GroupMembers; +use users::Entity as Users; + +pub async fn create_group( + db: &DatabaseConnection, + name: &str, +) -> Result { + let new_group = groups::ActiveModel { + name: Set(name.to_string()), + ..Default::default() + }; + + new_group.insert(db).await +} + +pub async fn get_all_groups( + db: &DatabaseConnection, +) -> Result, sea_orm::DbErr> { + Groups::find().all(db).await +} + +pub async fn find_group_by_id( + db: &DatabaseConnection, + id: i32, +) -> Result, sea_orm::DbErr> { + Groups::find_by_id(id).one(db).await +} + +pub async fn delete_group( + db: &DatabaseConnection, + id: i32, +) -> Result { + Groups::delete_by_id(id).exec(db).await +} + +pub async fn update_group_name( + db: &DatabaseConnection, + id: i32, + new_name: &str, +) -> Result, sea_orm::DbErr> { + let group = find_group_by_id(db, id).await?; + + match group { + Some(group) => { + let mut active_group = group.into_active_model(); + active_group.name = Set(new_name.to_string()); + + let updated_group = active_group.update(db).await?; + Ok(Some(updated_group)) + } + None => Ok(None), + } +} + +pub async fn add_member_to_group( + db: &DatabaseConnection, + group_id: i32, + user_id: i32, +) -> Result { + let new_member = group_members::ActiveModel { + group_id: Set(group_id), + user_id: Set(user_id), + ..Default::default() + }; + + new_member.insert(db).await +} + +pub async fn remove_member_from_group( + db: &DatabaseConnection, + group_id: i32, + user_id: i32, +) -> Result { + GroupMembers::delete_many() + .filter(group_members::Column::GroupId.eq(group_id)) + .filter(group_members::Column::UserId.eq(user_id)) + .exec(db) + .await +} + +pub async fn get_group_members( + db: &DatabaseConnection, + group_id: i32, +) -> Result, sea_orm::DbErr> { + Users::find() + .inner_join(group_members::Entity) + .filter(group_members::Column::GroupId.eq(group_id)) + .all(db) + .await +} \ No newline at end of file diff --git a/src/services/mod.rs b/src/services/mod.rs index b975176..f1b0920 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -3,3 +3,4 @@ pub mod balance; pub mod simplify; pub mod split; pub mod user_service; +pub mod group_service; diff --git a/src/services/user_service.rs b/src/services/user_service.rs index a1c085b..5e8b9c7 100644 --- a/src/services/user_service.rs +++ b/src/services/user_service.rs @@ -1,6 +1,9 @@ use super::auth_service::{hash_password, verify_password}; use crate::{entities::users, models::user::UserId}; -use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Set}; +use sea_orm::{ + ActiveModelTrait, ColumnTrait, DatabaseConnection, DeleteResult, EntityTrait, + IntoActiveModel, QueryFilter, Set, +}; use users::Entity as Users; // sign up @@ -70,3 +73,67 @@ pub async fn login_user( } } } + +pub async fn delete_user( + db: &DatabaseConnection, + id: UserId, +) -> Result { + Users::delete_by_id(id).exec(db).await +} + +pub async fn update_user_name( + db: &DatabaseConnection, + id: UserId, + new_name: &str, +) -> Result, sea_orm::DbErr> { + let user = find_user_by_id(db, id).await?; + + match user { + Some(user) => { + let mut active_user = user.into_active_model(); + active_user.name = Set(new_name.to_string()); + + let updated_user = active_user.update(db).await?; + Ok(Some(updated_user)) + } + None => Ok(None), + } +} + +pub async fn update_user_email( + db: &DatabaseConnection, + id: UserId, + new_email: &str, +) -> Result, sea_orm::DbErr> { + let user = find_user_by_id(db, id).await?; + + match user { + Some(user) => { + let mut active_user = user.into_active_model(); + active_user.email = Set(new_email.to_string()); + + let updated_user = active_user.update(db).await?; + Ok(Some(updated_user)) + } + None => Ok(None), + } +} + +pub async fn update_user_password( + db: &DatabaseConnection, + id: UserId, + new_password: &str, +) -> Result, sea_orm::DbErr> { + let user = find_user_by_id(db, id).await?; + + match user { + Some(user) => { + let mut active_user = user.into_active_model(); + active_user.password_hash = Set(hash_password(new_password)); + + let updated_user = active_user.update(db).await?; + Ok(Some(updated_user)) + } + None => Ok(None), + } +} \ No newline at end of file