Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The Cost Sharing application helps groups of people (roommates, friends, project

### User Management
- **Google OAuth Authentication**: Users log in with their Google account
- **Placeholder Accounts**: Invite people to groups before they create an account - they'll be activated when they first log in
- **Flexible User Creation**: Users can be added to groups before they log in - accounts are created as needed
- **Multi-Group Membership**: Users can belong to multiple groups simultaneously

### Expense Splitting
Expand Down
15 changes: 6 additions & 9 deletions docs/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ paths:
tags:
- Groups
summary: Add member to group
description: Adds a member to the group by email. Creates placeholder account if user hasn't logged in yet. When a placeholder user logs in for the first time via OAuth, their account is activated with the email and name from OAuth. Caller must be a member of the group.
description: Adds a member to the group by email. Creates user account if user doesn't exist yet. Caller must be a member of the group.
parameters:
- $ref: '#/components/parameters/GroupId'
requestBody:
Expand Down Expand Up @@ -253,9 +253,6 @@ paths:
properties:
member:
$ref: '#/components/schemas/User'
isNewUser:
type: boolean
description: True if a placeholder account was created
'400':
description: Validation error
content:
Expand Down Expand Up @@ -299,7 +296,7 @@ paths:
tags:
- Groups
summary: Remove member from group
description: Removes a member from the group. Users can remove themselves, or any member can remove others. A member cannot be removed if they are the group creator or if they are involved in any expenses (either as paidBy or in splitBetween for any expense in the group).
description: Removes a member from the group. A member can remove themselves, or the group creator can remove any member. A member cannot be removed if they are involved in any expenses (either as paidBy or in splitBetween for any expense in the group).
parameters:
- $ref: '#/components/parameters/GroupId'
- name: userId
Expand All @@ -325,17 +322,17 @@ paths:
error: "Resource not found"
message: "User not found"
'409':
description: Cannot remove member. Either the member is the group creator, or the member is involved in expenses (either as paidBy or in splitBetween for at least one expense in the group).
description: Cannot remove member. Either the authenticated user is not removing themselves and is not the group creator, or the member is involved in expenses (either as paidBy or in splitBetween for at least one expense in the group).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
examples:
creator:
summary: Cannot remove group creator
not_authorized:
summary: Cannot remove member - must be removing yourself or be the group creator
value:
error: "Conflict"
message: "Cannot remove group creator"
message: "You can only remove yourself, or you must be the group creator to remove other members"
involved_in_expenses:
summary: Cannot remove member involved in expenses
value:
Expand Down
14 changes: 7 additions & 7 deletions docs/sample-data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ PRAGMA foreign_keys = ON;
-- USERS
-- ============================================================================

INSERT INTO users (id, email, name, is_placeholder, oauth_provider_id) VALUES
(1, 'alice@school.edu', 'Alice', 0, '123456789012345678901'),
(2, 'bob@school.edu', 'Bob', 1, NULL),
(3, 'charlie@school.edu', 'Charlie', 0, '987654321098765432109'),
(4, 'david@school.edu', 'David', 0, '456789012345678901234'),
(5, 'eve@school.edu', 'Eve', 0, '789012345678901234567'),
(6, 'frank@school.edu', 'Frank', 1, NULL);
INSERT INTO users (id, email, name) VALUES
(1, 'alice@school.edu', 'Alice'),
(2, 'bob@school.edu', 'Bob'),
(3, 'charlie@school.edu', 'Charlie'),
(4, 'david@school.edu', 'David'),
(5, 'eve@school.edu', 'Eve'),
(6, 'frank@school.edu', 'Frank');

-- ============================================================================
-- GROUPS
Expand Down
41 changes: 18 additions & 23 deletions docs/sample-dataset.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,31 @@ This document provides example data for a collection of users in three different

## Users

In addition to a name, email address (which must be unique), and list of groups, each user has:
Each user has a name, email address (which must be unique), and list of groups.

- **Placeholder Account**: This flag indicates whether the user has never logged in (T = placeholder, F = active user who has logged in).
- **OAuth Provider ID**: This value is provided by Google OAuth to identify the user (it is `NULL` for placeholder accounts who never logged in).



| Name | Email | Placeholder Account | OAuth Provider ID | Group Membership |
|------|-------|---------|-------------------|------------------|
| Alice | alice@school.edu | F | `123456789012345678901` | Group 1 (creator), Group 2 |
| Bob | bob@school.edu | T | NULL | Group 1 |
| Charlie | charlie@school.edu | F | `987654321098765432109` | Group 2 (creator) |
| David | david@school.edu | F | `456789012345678901234` | Group 2 |
| Eve | eve@school.edu | F | `789012345678901234567` | Group 3 (creator) |
| Frank | frank@school.edu | T | NULL | Group 3 |
| Name | Email | Group Membership |
|------|-------|------------------|
| Alice | alice@school.edu | Group 1 (creator), Group 2 |
| Bob | bob@school.edu | Group 1 |
| Charlie | charlie@school.edu | Group 2 (creator) |
| David | david@school.edu | Group 2 |
| Eve | eve@school.edu | Group 3 (creator) |
| Frank | frank@school.edu | Group 3 |


---

## Group 1: Empty Group with Placeholder Member
## Group 1: Empty Group

This group is made up of two users, one of which has never logged in. There are no expenses associated with the group.
This group is made up of two users. There are no expenses associated with the group.


- **Name**: "Weekend Trip Planning"
- **Description**: "Planning expenses for upcoming weekend getaway"
- **Creator**: Alice (User ID: 1)
- **Members**:
- Alice (User ID: 1) - Active user, group creator
- Bob (User ID: 2) - Placeholder user (hasn't logged in yet)
- Alice (User ID: 1) - Group creator
- Bob (User ID: 2)

### Expenses

Expand All @@ -61,7 +56,7 @@ are combined when balances are computed.
- **Name**: "Roommates Spring 2025"
- **Description**: "Shared expenses for apartment 4B"
- **Creator**: Charlie (User ID: 3)
- **Members** (all active, non-placeholder):
- **Members**:
- Charlie (User ID: 3) - Group creator
- Alice (User ID: 1) - Also a member of Group 1 (cross-group membership)
- David (User ID: 4)
Expand Down Expand Up @@ -122,17 +117,17 @@ are combined when balances are computed.

---

## Group 3: Group with Placeholder Member and Expense
## Group 3: Group with Expense

This group has two members, one of which has never logged in. The creator of the group has already added an expense.
This group has two members. The creator of the group has already added an expense.

### Group Details
- **Name**: "Project Team Expenses"
- **Description**: "Team project collaboration costs"
- **Creator**: Eve (User ID: 5)
- **Members**:
- Eve (User ID: 5) - Active user, group creator
- Frank (User ID: 6) - Placeholder user (hasn't logged in yet)
- Eve (User ID: 5) - Group creator
- Frank (User ID: 6)

### Expenses

Expand Down
70 changes: 3 additions & 67 deletions docs/schema-sqlite.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
-- Cost Sharing API Database Schema
-- SQLite3 Schema for Flask Application (Simplified)
-- SQLite3 Schema for Flask Application
--
-- This schema implements the data model for the Cost Sharing API.
-- Simplified for small-scale class project (handful of users/groups, few hundred expenses).
--
-- All monetary amounts are stored as NUMERIC(10,2) for precision.
-- Expense dates are stored as DATE (YYYY-MM-DD format).
Expand All @@ -14,16 +13,12 @@ PRAGMA foreign_keys = ON;
-- ============================================================================
-- USERS TABLE
-- ============================================================================
-- Stores user accounts. Users can be created via OAuth or as placeholders
-- when added to a group before they've logged in.
-- Stores user accounts. Users can be created when added to a group.

CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
is_placeholder INTEGER NOT NULL DEFAULT 0, -- 0 = false, 1 = true
-- OAuth provider ID (for Google OAuth user identification)
oauth_provider_id TEXT
name TEXT NOT NULL
);

-- ============================================================================
Expand Down Expand Up @@ -79,62 +74,3 @@ CREATE TABLE expense_participants (
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
PRIMARY KEY (expense_id, user_id)
);

-- ============================================================================
-- NOTES FOR IMPLEMENTATION
-- ============================================================================
--
-- 1. ID GENERATION:
-- - IDs are auto-generated by SQLite using AUTOINCREMENT
-- - Sequential integers: 1, 2, 3, ...
-- - No need to generate IDs in application code
-- - After INSERT, get the ID: cursor.lastrowid or db.session.commit() then object.id
-- - In SQLAlchemy: user = User(...); db.session.add(user); db.session.commit(); user.id is set
--
-- 2. DATE HANDLING:
-- - SQLite stores DATE as ISO 8601 date strings: "2025-01-10"
-- - Expense dates use YYYY-MM-DD format
-- - In Python: use date objects or strings in "YYYY-MM-DD" format
--
-- 3. BOOLEAN VALUES:
-- - Store as INTEGER: 0 = false, 1 = true
-- - In Python: use bool() to convert, or store 0/1 directly
-- - SQLAlchemy's Boolean type handles conversion automatically
--
-- 4. NUMERIC PRECISION:
-- - NUMERIC(10,2) stores up to 99,999,999.99
-- - SQLite will enforce precision
-- - In Python: use Decimal type for calculations, convert to float for JSON
--
-- 5. VALIDATION RULES (enforced in application layer):
-- - paidBy must be in splitBetween (expense_participants)
-- - All users in splitBetween must be group members
-- - paidBy must be a group member
-- - Cannot delete group if expenses exist (check expenses table)
-- - Cannot remove member if they're in expense_participants or paid_by
--
-- 6. TRANSACTIONS:
-- - SQLite supports transactions: BEGIN TRANSACTION; ... COMMIT;
-- - Use for: creating expense + participants, adding member + creating placeholder user
-- - In Flask: use db.session.begin() or @db.transaction decorator
--
-- 7. CASCADING DELETES:
-- - Deleting a group deletes all expenses and group_members (CASCADE)
-- - Deleting an expense deletes all expense_participants (CASCADE)
-- - Deleting a user is RESTRICTED if they're involved in expenses or created groups
--
-- 8. PERFORMANCE:
-- - This simplified schema omits indexes, views, and triggers for simplicity
-- - At small scale (handful of users/groups, few hundred expenses), performance is excellent
-- - Full table scans are fast enough (< 5ms for typical queries)
-- - If needed later, see full_db/ folder for optimized version with indexes
--
-- 9. FLASK SETUP:
-- - Use Flask-SQLAlchemy for ORM (recommended)
-- - Or use sqlite3 directly with connection management
-- - Enable foreign keys: conn.execute("PRAGMA foreign_keys = ON")
--
-- 10. BACKUP:
-- - SQLite database is a single file: cost_sharing.db
-- - Easy to backup: just copy the file
-- - Can use .dump command: sqlite3 cost_sharing.db .dump > backup.sql
59 changes: 12 additions & 47 deletions docs/usecases.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,15 @@ This document outlines the complete list of use cases for the Cost Sharing appli
3. User authenticates with Google
4. System receives OAuth callback with authorization code
5. System exchanges code for user information
6. System matches OAuth provider ID to existing user account
6. System matches email to existing user account
7. System generates and returns JWT token
8. User is logged in and can access the application

**Postcondition**: User is authenticated with valid token

---

### UC-AUTH-003: Placeholder User Activation
**Actor**: Placeholder User (invited but never logged in)
**Precondition**: User was added to a group as a placeholder (email/name only, no OAuth)
**Main Flow**:
1. Placeholder user initiates Google OAuth login
2. System receives OAuth callback with authorization code
3. System exchanges code for user information
4. System matches email from OAuth to existing placeholder account
5. System activates placeholder account by:
- Setting `is_placeholder` to false
- Setting `oauth_provider_id` from OAuth
- Updating name if different from placeholder
6. System generates and returns JWT token
7. User is logged in and can access the application

**Postcondition**: Placeholder account is activated, user is authenticated

---

### UC-AUTH-004: Get Current User Information
### UC-AUTH-003: Get Current User Information
**Actor**: Authenticated User
**Precondition**: User has valid authentication token
**Main Flow**:
Expand Down Expand Up @@ -132,41 +113,25 @@ This document outlines the complete list of use cases for the Cost Sharing appli

---

### UC-GROUP-005: Add Member to Group (Existing User)
### UC-GROUP-005: Add Member to Group
**Actor**: Authenticated User (must be group member)
**Precondition**: User is logged in, is a member of the group, and target user already has an account
**Precondition**: User is logged in and is a member of the group
**Main Flow**:
1. User provides email and name of person to add
2. System validates email format
3. System checks if user with email already exists
4. System checks if user is already a member of the group
- If user is already a member: System returns 409 Conflict with error message
5. If not already a member, system adds user to group
6. System returns member information and `isNewUser: false`

**Postcondition**: Existing user is added as group member

---

### UC-GROUP-006: Add Member to Group (New Placeholder User)
**Actor**: Authenticated User (must be group member)
**Precondition**: User is logged in, is a member of the group, and target user does not have an account
**Main Flow**:
1. User provides email and name of person to add
2. System validates email format
3. System checks if user with email exists
4. If user does not exist, system creates placeholder account:
4. If user does not exist, system creates new user account:
- Email and name from input
- `is_placeholder` = true
- `oauth_provider_id` = NULL
5. System adds placeholder user to group
6. System returns member information and `isNewUser: true`
5. System checks if user is already a member of the group
- If user is already a member: System returns 409 Conflict with error message
6. If not already a member, system adds user to group
7. System returns member information

**Postcondition**: Placeholder user account is created and added as group member
**Postcondition**: User (existing or newly created) is added as group member

---

### UC-GROUP-007: Remove Member from Group (Self)
### UC-GROUP-006: Remove Member from Group (Self)
**Actor**: Authenticated User (removing themselves)
**Precondition**: User is logged in, is a member of the group, is not the creator, and is not involved in any expenses
**Main Flow**:
Expand All @@ -183,7 +148,7 @@ This document outlines the complete list of use cases for the Cost Sharing appli

---

### UC-GROUP-008: Remove Member from Group (Other Member)
### UC-GROUP-007: Remove Member from Group (Other Member)
**Actor**: Authenticated User (removing another member)
**Precondition**: User is logged in, is a member of the group, target user is not the creator, and target user is not involved in any expenses
**Main Flow**:
Expand Down
44 changes: 44 additions & 0 deletions src/cost_sharing/cost_sharing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Application layer for Cost Sharing system."""


class CostSharing:
"""Application layer for Cost Sharing system."""

def __init__(self, storage):
"""
Initialize the application layer with a storage implementation.

Args:
storage: CostStorage implementation (e.g., InMemoryCostStorage, DBCostStorage)
"""
self._storage = storage

def get_user_by_id(self, user_id):
"""
Get user by their ID.

Args:
user_id: User ID

Returns:
User object

Raises:
UserNotFoundError: If user doesn't exist
"""
return self._storage.get_user_by_id(user_id)

def get_or_create_user(self, email, name):
"""
Get existing user or create new user.

Args:
email: User's email
name: User's name

Returns:
User object (existing or newly created)
"""
if self._storage.is_user(email):
return self._storage.get_user_by_email(email)
return self._storage.create_user(email, name)
Loading