import uuid from typing import Optional from sqlalchemy.orm import Session from fastapi import HTTPException, status from backend.app.repositories.bookings import BookingRepository from backend.app.repositories.wallet import WalletRepository from backend.app.repositories.users import UserRepository from backend.app.repositories.alerts import AlertRepository from backend.app.models.bookings import Booking from backend.app.models.reviews import Review class BookingService: def __init__(self, db: Session): self.db = db self.booking_repo = BookingRepository(db) self.wallet_repo = WalletRepository(db) self.user_repo = UserRepository(db) self.alert_repo = AlertRepository(db) def create_booking(self, customer_id: uuid.UUID, worker_id: uuid.UUID, job_id: Optional[uuid.UUID] = None) -> Booking: """Create a booking, placing the amount in escrow hold.""" worker = self.user_repo.get_by_id(worker_id) if not worker or worker.role != "worker" or not worker.worker_profile: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Worker profile not found." ) customer_profile = self.wallet_repo.get_customer_profile(customer_id) wallet_balance = customer_profile.wallet_balance if customer_profile else 0 # Determine amount: direct booking uses daily rate amount = worker.worker_profile.rate if wallet_balance < amount: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Insufficient wallet balance ({wallet_balance} INR). Escrow hold requires {amount} INR. Please top up." ) # Deduct wallet balance from customer self.wallet_repo.update_balance(customer_id, -amount) # Create booking booking = self.booking_repo.create( customer_id=customer_id, worker_id=worker_id, amount=amount, job_id=job_id ) # Record escrow hold in transaction ledger self.wallet_repo.add_transaction( user_id=customer_id, booking_id=booking.id, label=f"Escrow hold for booking {booking.code}", amount=-amount, type_="hold" ) # Update job status if linked to a job post if job_id: from backend.app.repositories.jobs import JobRepository JobRepository(self.db).update_status(job_id, "ACCEPTED") return booking def advance_booking_status(self, booking_id: uuid.UUID, new_status: str, actor_id: uuid.UUID) -> Booking: """Advance the booking lifecycle state machine.""" booking = self.booking_repo.get_by_id(booking_id) if not booking: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Booking not found." ) # Verify access: only customer, worker, or admin can update status if actor_id not in (booking.customer_id, booking.worker_id): actor = self.user_repo.get_by_id(actor_id) if not actor or actor.role != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="You are not authorized to update this booking." ) valid_states = ["POSTED", "ACCEPTED", "IN_PROGRESS", "COMPLETED", "RATED"] # State machine validations current_status = booking.status if current_status == "COMPLETED" and new_status not in ("RATED", "DISPUTED"): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Completed bookings can only be Rated or Disputed." ) # Update status self.booking_repo.update_status(booking_id, new_status) # Business logic for completion: release payout to worker wallet if new_status == "COMPLETED" and current_status != "COMPLETED": # Calculate worker payout: 95% of booking amount (5% platform commission) commission = round(booking.amount * 0.05) payout = booking.amount - commission # Release payout to worker # For simplicity in demo, we record this in the worker's wallet or ledger transaction self.wallet_repo.update_balance(booking.worker_id, payout) # Record release ledger transactions self.wallet_repo.add_transaction( user_id=booking.worker_id, booking_id=booking.id, label=f"Completed job payout for {booking.code} (95% release)", amount=payout, type_="release" ) # Record platform fee self.wallet_repo.add_transaction( user_id=booking.customer_id, booking_id=booking.id, label=f"Escrow release for {booking.code} (5% platform fee deducted)", amount=-booking.amount, type_="release" ) # Update worker profiles jobs counter worker = self.user_repo.get_by_id(booking.worker_id) if worker and worker.worker_profile: worker.worker_profile.completed_jobs += 1 self.db.commit() # Update linked job status if booking.job_id: from backend.app.repositories.jobs import JobRepository job_status = "IN_PROGRESS" if new_status == "IN_PROGRESS" else "COMPLETED" if new_status in ("COMPLETED", "RATED") else "ACCEPTED" JobRepository(self.db).update_status(booking.job_id, job_status) return booking def dispute_booking(self, booking_id: uuid.UUID, reason: str, actor_id: uuid.UUID) -> Booking: """File a dispute for a booking, alerting the Admin queue.""" booking = self.booking_repo.get_by_id(booking_id) if not booking: raise HTTPException(status_code=404, detail="Booking not found.") if actor_id not in (booking.customer_id, booking.worker_id): raise HTTPException(status_code=403, detail="Not authorized.") # Update booking status self.booking_repo.update_status(booking_id, "DISPUTED") # Generate admin alert self.alert_repo.create( type_="Dispute", text=f"Dispute filed on {booking.code} by customer. Reason: {reason}", severity="red" ) return booking def refund_booking(self, booking_id: uuid.UUID) -> Booking: """Admin action: Resolve dispute by refunding the customer.""" booking = self.booking_repo.get_by_id(booking_id) if not booking: raise HTTPException(status_code=404, detail="Booking not found.") # Refund amount to customer wallet self.wallet_repo.update_balance(booking.customer_id, booking.amount) # Log refund in ledger self.wallet_repo.add_transaction( user_id=booking.customer_id, booking_id=booking.id, label=f"Refund for cancelled booking {booking.code}", amount=booking.amount, type_="refund" ) # Update status self.booking_repo.update_status(booking_id, "CANCELLED") if booking.job_id: from backend.app.repositories.jobs import JobRepository JobRepository(self.db).update_status(booking.job_id, "POSTED") return booking def submit_review(self, booking_id: uuid.UUID, rating: int, comment: Optional[str], reviewer_id: uuid.UUID) -> Review: """Rate a booking worker and update their average rating profile.""" booking = self.booking_repo.get_by_id(booking_id) if not booking: raise HTTPException(status_code=404, detail="Booking not found.") if reviewer_id != booking.customer_id: raise HTTPException(status_code=403, detail="Only customers can rate their bookings.") # Write review review = self.booking_repo.add_review( booking_id=booking_id, reviewer_id=reviewer_id, reviewee_id=booking.worker_id, rating=rating, comment=comment ) # Advance status to rated self.booking_repo.update_status(booking_id, "RATED") # Update worker rating in profile worker = self.user_repo.get_by_id(booking.worker_id) if worker and worker.worker_profile: # Calculate average rating reviews = self.db.query(Review).filter(Review.reviewee_id == booking.worker_id).all() avg_rating = sum(r.rating for r in reviews) / len(reviews) worker.worker_profile.rating = round(avg_rating, 2) self.db.commit() return review