diff --git a/backend/models.py b/backend/models.py index 261d303..20d54e9 100644 --- a/backend/models.py +++ b/backend/models.py @@ -41,15 +41,16 @@ class Permission(str, PyEnum): MANAGE_INVITES = "manage_invites" VIEW_OWN_INVITES = "view_own_invites" - # Booking permissions (regular users) - BOOK_APPOINTMENT = "book_appointment" - VIEW_OWN_APPOINTMENTS = "view_own_appointments" - CANCEL_OWN_APPOINTMENT = "cancel_own_appointment" + # Exchange permissions (regular users) + CREATE_EXCHANGE = "create_exchange" + VIEW_OWN_EXCHANGES = "view_own_exchanges" + CANCEL_OWN_EXCHANGE = "cancel_own_exchange" - # Availability/Appointments permissions (admin) + # Availability/Exchange permissions (admin) MANAGE_AVAILABILITY = "manage_availability" - VIEW_ALL_APPOINTMENTS = "view_all_appointments" - CANCEL_ANY_APPOINTMENT = "cancel_any_appointment" + VIEW_ALL_EXCHANGES = "view_all_exchanges" + CANCEL_ANY_EXCHANGE = "cancel_any_exchange" + COMPLETE_EXCHANGE = "complete_exchange" class InviteStatus(str, PyEnum): @@ -60,14 +61,6 @@ class InviteStatus(str, PyEnum): REVOKED = "revoked" -class AppointmentStatus(str, PyEnum): - """Status of an appointment.""" - - BOOKED = "booked" - CANCELLED_BY_USER = "cancelled_by_user" - CANCELLED_BY_ADMIN = "cancelled_by_admin" - - class ExchangeStatus(str, PyEnum): """Status of an exchange trade.""" @@ -92,24 +85,25 @@ ROLE_REGULAR = "regular" # Role definitions with their permissions ROLE_DEFINITIONS: dict[str, RoleConfig] = { ROLE_ADMIN: { - "description": "Administrator with audit/invite/appointment access", + "description": "Administrator with audit/invite/exchange access", "permissions": [ Permission.VIEW_AUDIT, Permission.FETCH_PRICE, Permission.MANAGE_INVITES, Permission.MANAGE_AVAILABILITY, - Permission.VIEW_ALL_APPOINTMENTS, - Permission.CANCEL_ANY_APPOINTMENT, + Permission.VIEW_ALL_EXCHANGES, + Permission.CANCEL_ANY_EXCHANGE, + Permission.COMPLETE_EXCHANGE, ], }, ROLE_REGULAR: { - "description": "Regular user with profile, invite, and booking access", + "description": "Regular user with profile, invite, and exchange access", "permissions": [ Permission.MANAGE_OWN_PROFILE, Permission.VIEW_OWN_INVITES, - Permission.BOOK_APPOINTMENT, - Permission.VIEW_OWN_APPOINTMENTS, - Permission.CANCEL_OWN_APPOINTMENT, + Permission.CREATE_EXCHANGE, + Permission.VIEW_OWN_EXCHANGES, + Permission.CANCEL_OWN_EXCHANGE, ], }, } @@ -303,33 +297,6 @@ class Availability(Base): ) -class Appointment(Base): - """User appointment bookings.""" - - __tablename__ = "appointments" - __table_args__ = (UniqueConstraint("slot_start", name="uq_appointment_slot_start"),) - - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - user_id: Mapped[int] = mapped_column( - Integer, ForeignKey("users.id"), nullable=False, index=True - ) - user: Mapped[User] = relationship("User", foreign_keys=[user_id], lazy="joined") - slot_start: Mapped[datetime] = mapped_column( - DateTime(timezone=True), nullable=False, index=True - ) - slot_end: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) - note: Mapped[str | None] = mapped_column(String(144), nullable=True) - status: Mapped[AppointmentStatus] = mapped_column( - Enum(AppointmentStatus), nullable=False, default=AppointmentStatus.BOOKED - ) - created_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), default=lambda: datetime.now(UTC) - ) - cancelled_at: Mapped[datetime | None] = mapped_column( - DateTime(timezone=True), nullable=True - ) - - class PriceHistory(Base): """Price history records from external exchanges.""" diff --git a/backend/routes/exchange.py b/backend/routes/exchange.py index 32c568c..c8f1f32 100644 --- a/backend/routes/exchange.py +++ b/backend/routes/exchange.py @@ -180,7 +180,7 @@ def _to_exchange_response( @router.get("/price", response_model=ExchangePriceResponse) async def get_exchange_price( db: AsyncSession = Depends(get_db), - _current_user: User = Depends(require_permission(Permission.BOOK_APPOINTMENT)), + _current_user: User = Depends(require_permission(Permission.CREATE_EXCHANGE)), ) -> ExchangePriceResponse: """ Get the current BTC/EUR price for trading. @@ -275,7 +275,7 @@ async def get_exchange_price( async def create_exchange( request: ExchangeRequest, db: AsyncSession = Depends(get_db), - current_user: User = Depends(require_permission(Permission.BOOK_APPOINTMENT)), + current_user: User = Depends(require_permission(Permission.CREATE_EXCHANGE)), ) -> ExchangeResponse: """ Create a new exchange trade booking. @@ -415,7 +415,7 @@ trades_router = APIRouter(prefix="/api/trades", tags=["trades"]) @trades_router.get("", response_model=list[ExchangeResponse]) async def get_my_trades( db: AsyncSession = Depends(get_db), - current_user: User = Depends(require_permission(Permission.VIEW_OWN_APPOINTMENTS)), + current_user: User = Depends(require_permission(Permission.VIEW_OWN_EXCHANGES)), ) -> list[ExchangeResponse]: """Get the current user's exchanges, sorted by date (newest first).""" result = await db.execute( @@ -432,7 +432,7 @@ async def get_my_trades( async def cancel_my_trade( exchange_id: int, db: AsyncSession = Depends(get_db), - current_user: User = Depends(require_permission(Permission.CANCEL_OWN_APPOINTMENT)), + current_user: User = Depends(require_permission(Permission.CANCEL_OWN_EXCHANGE)), ) -> ExchangeResponse: """Cancel one of the current user's exchanges.""" # Get the exchange with eager loading of user relationship @@ -512,7 +512,7 @@ def _to_admin_exchange_response(exchange: Exchange) -> AdminExchangeResponse: @admin_trades_router.get("/upcoming", response_model=list[AdminExchangeResponse]) async def get_upcoming_trades( db: AsyncSession = Depends(get_db), - _current_user: User = Depends(require_permission(Permission.VIEW_ALL_APPOINTMENTS)), + _current_user: User = Depends(require_permission(Permission.VIEW_ALL_EXCHANGES)), ) -> list[AdminExchangeResponse]: """Get all upcoming booked trades, sorted by slot time ascending.""" now = datetime.now(UTC) @@ -539,7 +539,7 @@ async def get_past_trades( end_date: date | None = None, user_search: str | None = None, db: AsyncSession = Depends(get_db), - _current_user: User = Depends(require_permission(Permission.VIEW_ALL_APPOINTMENTS)), + _current_user: User = Depends(require_permission(Permission.VIEW_ALL_EXCHANGES)), ) -> list[AdminExchangeResponse]: """ Get past trades with optional filters. @@ -599,9 +599,7 @@ async def get_past_trades( async def complete_trade( exchange_id: int, db: AsyncSession = Depends(get_db), - _current_user: User = Depends( - require_permission(Permission.CANCEL_ANY_APPOINTMENT) - ), + _current_user: User = Depends(require_permission(Permission.CANCEL_ANY_EXCHANGE)), ) -> AdminExchangeResponse: """Mark a trade as completed. Only possible after slot time has passed.""" @@ -647,9 +645,7 @@ async def complete_trade( async def mark_no_show( exchange_id: int, db: AsyncSession = Depends(get_db), - _current_user: User = Depends( - require_permission(Permission.CANCEL_ANY_APPOINTMENT) - ), + _current_user: User = Depends(require_permission(Permission.CANCEL_ANY_EXCHANGE)), ) -> AdminExchangeResponse: """Mark a trade as no-show. Only possible after slot time has passed.""" @@ -693,9 +689,7 @@ async def mark_no_show( async def admin_cancel_trade( exchange_id: int, db: AsyncSession = Depends(get_db), - _current_user: User = Depends( - require_permission(Permission.CANCEL_ANY_APPOINTMENT) - ), + _current_user: User = Depends(require_permission(Permission.CANCEL_ANY_EXCHANGE)), ) -> AdminExchangeResponse: """Cancel any trade (admin only).""" diff --git a/backend/schemas.py b/backend/schemas.py index aee21b2..827cc4c 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -6,7 +6,6 @@ from typing import Generic, TypeVar from pydantic import BaseModel, EmailStr, field_validator from models import InviteStatus, Permission -from shared_constants import NOTE_MAX_LENGTH class UserCredentials(BaseModel): @@ -173,51 +172,6 @@ class CopyAvailabilityRequest(BaseModel): # ============================================================================= -class BookableSlot(BaseModel): - """A bookable 15-minute slot.""" - - start_time: datetime - end_time: datetime - - -class AvailableSlotsResponse(BaseModel): - """Response for available slots on a given date.""" - - date: date - slots: list[BookableSlot] - - -class BookingRequest(BaseModel): - """Request to book an appointment.""" - - slot_start: datetime - note: str | None = None - - @field_validator("note") - @classmethod - def validate_note_length(cls, v: str | None) -> str | None: - if v is not None and len(v) > NOTE_MAX_LENGTH: - raise ValueError(f"Note must be at most {NOTE_MAX_LENGTH} characters") - return v - - -class AppointmentResponse(BaseModel): - """Response model for an appointment.""" - - id: int - user_id: int - user_email: str - slot_start: datetime - slot_end: datetime - note: str | None - status: str - created_at: datetime - cancelled_at: datetime | None - - -PaginatedAppointments = PaginatedResponse[AppointmentResponse] - - # ============================================================================= # Price History Schemas # ============================================================================= diff --git a/backend/shared_constants.py b/backend/shared_constants.py index 4fe7c5a..5c5e2ed 100644 --- a/backend/shared_constants.py +++ b/backend/shared_constants.py @@ -6,13 +6,10 @@ from pathlib import Path _constants_path = Path(__file__).parent.parent / "shared" / "constants.json" _constants = json.loads(_constants_path.read_text()) -# Booking constants -SLOT_DURATION_MINUTES: int = _constants["booking"]["slotDurationMinutes"] -MIN_ADVANCE_DAYS: int = _constants["booking"]["minAdvanceDays"] -MAX_ADVANCE_DAYS: int = _constants["booking"]["maxAdvanceDays"] -NOTE_MAX_LENGTH: int = _constants["booking"]["noteMaxLength"] - # Exchange constants +SLOT_DURATION_MINUTES: int = _constants["exchange"]["slotDurationMinutes"] +MIN_ADVANCE_DAYS: int = _constants["exchange"]["minAdvanceDays"] +MAX_ADVANCE_DAYS: int = _constants["exchange"]["maxAdvanceDays"] EUR_TRADE_MIN: int = _constants["exchange"]["eurTradeMin"] EUR_TRADE_MAX: int = _constants["exchange"]["eurTradeMax"] EUR_TRADE_INCREMENT: int = _constants["exchange"]["eurTradeIncrement"] diff --git a/backend/validate_constants.py b/backend/validate_constants.py index 40c9202..a6f7c83 100644 --- a/backend/validate_constants.py +++ b/backend/validate_constants.py @@ -6,7 +6,6 @@ from pathlib import Path from models import ( ROLE_ADMIN, ROLE_REGULAR, - AppointmentStatus, ExchangeStatus, InviteStatus, TradeDirection, @@ -42,15 +41,6 @@ def validate_shared_constants() -> None: f"Invite status mismatch. Expected: {expected_invite_statuses}, Got: {got}" ) - # Validate appointment statuses - expected_appointment_statuses = {s.name: s.value for s in AppointmentStatus} - if constants.get("appointmentStatuses") != expected_appointment_statuses: - got = constants.get("appointmentStatuses") - raise ValueError( - f"Appointment status mismatch. " - f"Expected: {expected_appointment_statuses}, Got: {got}" - ) - # Validate exchange statuses expected_exchange_statuses = {s.name: s.value for s in ExchangeStatus} if constants.get("exchangeStatuses") != expected_exchange_statuses: @@ -69,21 +59,12 @@ def validate_shared_constants() -> None: f"Expected: {expected_trade_directions}, Got: {got}" ) - # Validate booking constants exist with required fields - booking = constants.get("booking", {}) - required_booking_fields = [ - "slotDurationMinutes", - "maxAdvanceDays", - "minAdvanceDays", - "noteMaxLength", - ] - for field in required_booking_fields: - if field not in booking: - raise ValueError(f"Missing booking constant '{field}' in constants.json") - # Validate exchange constants exist with required fields exchange = constants.get("exchange", {}) required_exchange_fields = [ + "slotDurationMinutes", + "maxAdvanceDays", + "minAdvanceDays", "eurTradeMin", "eurTradeMax", "eurTradeIncrement", diff --git a/shared/constants.json b/shared/constants.json index 5f7ee5f..20acff4 100644 --- a/shared/constants.json +++ b/shared/constants.json @@ -8,11 +8,6 @@ "SPENT": "spent", "REVOKED": "revoked" }, - "appointmentStatuses": { - "BOOKED": "booked", - "CANCELLED_BY_USER": "cancelled_by_user", - "CANCELLED_BY_ADMIN": "cancelled_by_admin" - }, "exchangeStatuses": { "BOOKED": "booked", "COMPLETED": "completed", @@ -24,13 +19,10 @@ "BUY": "buy", "SELL": "sell" }, - "booking": { + "exchange": { "slotDurationMinutes": 15, "maxAdvanceDays": 30, "minAdvanceDays": 1, - "noteMaxLength": 144 - }, - "exchange": { "eurTradeMin": 100, "eurTradeMax": 3000, "eurTradeIncrement": 20,