import ulid from datetime import datetime from sqlalchemy.orm import Session from sqlalchemy import select from typing import Optional, List from decimal import Decimal from fastapi import HTTPException, status from app.models.ServiceModel import ServiceJob, ServiceAppointment, ServiceJobEvent from app.models.ServiceQuoteModel import ServiceJobQuote from app.models.ServicePaymentModel import ServicePayment from app.schemas.ServicePaymentSchema import RazorpayVerificationRequest from app.core.razorpay import razorpay_service class ServicePaymentService: def initiate_milestone_payment( self, db: Session, job_id: str, customer_id: str, payment_type: str, quote_id: Optional[str] = None ) -> dict: job = db.get(ServiceJob, job_id) if not job or job.customer_id != customer_id: raise HTTPException(status_code=404, detail="Service job not found") # Determine Amount based on payment type base_price = Decimal(str(job.base_price_snapshot if job.base_price_snapshot is not None else (job.service.base_price if job.service else 0.0))) amount = Decimal("0.00") if payment_type == "ADVANCE": # 20% of base price snapshot amount = base_price * Decimal("0.20") elif payment_type == "INSPECTION": amount = Decimal(str(job.inspection_fee_snapshot or 300.00)) elif payment_type == "ADDITIONAL": if not quote_id: raise HTTPException(status_code=400, detail="Quote ID required for additional damage payment") quote = db.get(ServiceJobQuote, quote_id) if not quote: raise HTTPException(status_code=404, detail="Quote not found") amount = Decimal(str(quote.additional_damage_amount)) elif payment_type == "FINAL": # Total quote amount minus advance paid stmt = select(ServiceJobQuote).where(ServiceJobQuote.service_job_id == job_id).order_by(ServiceJobQuote.version.desc()) latest_quote = db.execute(stmt).scalars().first() total_cost = Decimal(str(latest_quote.total if latest_quote else base_price)) # Find advance already paid paid_advance_stmt = select(ServicePayment).where( ServicePayment.service_job_id == job_id, ServicePayment.payment_type == "ADVANCE", ServicePayment.status == "CAPTURED" ) paid_advance = db.execute(paid_advance_stmt).scalars().all() advance_paid = sum(Decimal(str(p.amount)) for p in paid_advance) amount = total_cost - advance_paid else: raise HTTPException(status_code=400, detail="Invalid payment type") if amount <= 0: raise HTTPException(status_code=400, detail="Payment amount must be greater than zero") # Create Razorpay order amount_paise = int(amount * 100) rzp_order = razorpay_service.create_order(amount_paise, "INR") # Create payment record payment = ServicePayment( payment_id=str(ulid.ULID()), service_job_id=job_id, quote_id=quote_id, payment_type=payment_type, amount=amount, status="PENDING", provider="RAZORPAY", provider_order_id=rzp_order["id"] ) db.add(payment) db.commit() db.refresh(payment) from app.core.config.Config import settings return { "payment_id": payment.payment_id, "rzp_order_id": rzp_order["id"], "amount": amount_paise, "currency": "INR", "rzp_key_id": settings.RAZORPAY_KEY_ID } def verify_milestone_payment(self, db: Session, payload: RazorpayVerificationRequest) -> dict: # Verify Razorpay signature is_valid = razorpay_service.verify_payment_signature( rzp_order_id=payload.razorpay_order_id, rzp_payment_id=payload.razorpay_payment_id, rzp_signature=payload.razorpay_signature ) if not is_valid: raise HTTPException(status_code=400, detail="Invalid signature verification failed") # Find matching pending payment stmt = select(ServicePayment).where(ServicePayment.provider_order_id == payload.razorpay_order_id) payment = db.execute(stmt).scalar_one_or_none() if not payment: raise HTTPException(status_code=404, detail="Matching payment record not found") if payment.status == "CAPTURED": return {"message": "Payment already processed", "job_id": payment.service_job_id} payment.status = "CAPTURED" payment.provider_payment_id = payload.razorpay_payment_id payment.provider_signature = payload.razorpay_signature payment.paid_at = datetime.utcnow() # Update Job / Appointment depending on milestone type job = db.get(ServiceJob, payment.service_job_id) if payment.payment_type == "ADVANCE": job.status = "BOOKED" # Mark the appointment as CONFIRMED app_stmt = select(ServiceAppointment).where( ServiceAppointment.service_job_id == job.job_id, ServiceAppointment.status == "HELD" ) appt = db.execute(app_stmt).scalar_one_or_none() if appt: appt.status = "CONFIRMED" appt.confirmed_at = datetime.utcnow() elif payment.payment_type == "FINAL": job.status = "READY_FOR_DELIVERY" # Log timeline event event = ServiceJobEvent( event_id=str(ulid.ULID()), job_id=job.job_id, event_type="PAYMENT_RECEIVED", performed_by=job.customer_id, notes=f"Milestone payment {payment.payment_type} of {payment.amount} INR verified and processed." ) db.add(event) db.commit() return {"message": "Payment verified successfully", "job_id": job.job_id} def process_quote_rejection_refund(self, db: Session, job_id: str) -> dict: job = db.get(ServiceJob, job_id) if not job: raise HTTPException(status_code=404, detail="Service job not found") # 1. Fetch total captured advance paid paid_advances_stmt = select(ServicePayment).where( ServicePayment.service_job_id == job_id, ServicePayment.payment_type == "ADVANCE", ServicePayment.status == "CAPTURED" ) paid_advances = db.execute(paid_advances_stmt).scalars().all() advance_paid = sum(Decimal(str(p.amount)) for p in paid_advances) fee = Decimal(str(job.inspection_fee_snapshot or 300.00)) if advance_paid >= fee: # Settle inspection fee from advance settled_pmt = ServicePayment( payment_id=str(ulid.ULID()), service_job_id=job_id, payment_type="INSPECTION", amount=fee, status="SETTLED", provider="INTERNAL_SETTLEMENT" ) db.add(settled_pmt) # Refund remaining balance refund_amount = advance_paid - fee if refund_amount > 0: refund_pmt = ServicePayment( payment_id=str(ulid.ULID()), service_job_id=job_id, payment_type="REFUND", amount=refund_amount, status="REFUNDED", provider="RAZORPAY", provider_refund_id=f"rfnd_{ulid.ULID()}", refund_reason="REVISED_QUOTE_REJECTED", refunded_at=datetime.utcnow() ) db.add(refund_pmt) job.status = "RETURN_PENDING" else: if advance_paid > 0: settled_pmt = ServicePayment( payment_id=str(ulid.ULID()), service_job_id=job_id, payment_type="INSPECTION", amount=advance_paid, status="SETTLED", provider="INTERNAL_SETTLEMENT" ) db.add(settled_pmt) job.status = "INSPECTION_FEE_PENDING" event = ServiceJobEvent( event_id=str(ulid.ULID()), job_id=job_id, event_type="QUOTE_REJECTED", performed_by=job.customer_id, notes=f"Customer rejected revised quote. Inspection fee {fee} INR processed. Job status: {job.status}." ) db.add(event) db.commit() return { "message": "Quote rejection processed", "job_id": job.job_id, "status": job.status, "advance_paid": float(advance_paid), "inspection_fee": float(fee) }