Files / app /services /bookings.py
lea97338's picture
Upload 100 files (#1)
680fa2b
Raw
History Blame Contribute Delete
9.22 kB
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