Spaces:
Runtime error
Runtime error
| 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 | |