Files
TimeTracker/app/services/payment_service.py
T
Dries Peeters 3218ab012a feat: expand client portal and approval workflows
Add new client portal pages (dashboard, approvals, notifications, documents, reports) and extend API/routes/services to support client approvals, invoices/quotes views, and related notifications.

Update email templates and docs; add/adjust tests for new models/routes.
2026-01-02 07:52:32 +01:00

200 lines
7.4 KiB
Python

"""
Service for payment business logic.
"""
from typing import Optional, Dict, Any, List
from datetime import date
from decimal import Decimal
from app import db
from app.repositories import PaymentRepository, InvoiceRepository
from app.models import Payment, Invoice
from app.utils.db import safe_commit
from app.utils.event_bus import emit_event
from app.constants import WebhookEvent
class PaymentService:
"""Service for payment operations"""
def __init__(self):
self.payment_repo = PaymentRepository()
self.invoice_repo = InvoiceRepository()
def create_payment(
self,
invoice_id: int,
amount: Decimal,
payment_date: date,
received_by: int,
currency: Optional[str] = None,
method: Optional[str] = None,
reference: Optional[str] = None,
notes: Optional[str] = None,
status: str = "completed",
gateway_transaction_id: Optional[str] = None,
gateway_fee: Optional[Decimal] = None,
) -> Dict[str, Any]:
"""
Create a new payment.
Returns:
dict with 'success', 'message', and 'payment' keys
"""
# Validate invoice
invoice = self.invoice_repo.get_by_id(invoice_id)
if not invoice:
return {"success": False, "message": "Invoice not found", "error": "invalid_invoice"}
# Validate amount
if amount <= 0:
return {"success": False, "message": "Amount must be greater than zero", "error": "invalid_amount"}
# Get currency from invoice if not provided
if not currency:
currency = invoice.currency_code
# Create payment
payment = self.payment_repo.create(
invoice_id=invoice_id,
amount=amount,
currency=currency,
payment_date=payment_date,
method=method,
reference=reference,
notes=notes,
status=status,
received_by=received_by,
gateway_transaction_id=gateway_transaction_id,
gateway_fee=gateway_fee,
)
# Calculate net amount
payment.calculate_net_amount()
# Update invoice payment status if payment is completed
if status == "completed":
total_payments = self.payment_repo.get_total_for_invoice(invoice_id)
invoice.amount_paid = total_payments + amount
# Update payment status
if invoice.amount_paid >= invoice.total_amount:
invoice.payment_status = "fully_paid"
elif invoice.amount_paid > 0:
invoice.payment_status = "partially_paid"
else:
invoice.payment_status = "unpaid"
if not safe_commit("create_payment", {"invoice_id": invoice_id, "received_by": received_by}):
return {
"success": False,
"message": "Could not create payment due to a database error",
"error": "database_error",
}
# Emit domain event
emit_event("payment.created", {"payment_id": payment.id, "invoice_id": invoice_id, "amount": float(amount)})
# Notify client about payment received
if invoice.client_id and status == "completed":
try:
from app.services.client_notification_service import ClientNotificationService
notification_service = ClientNotificationService()
notification_service.notify_invoice_paid(invoice_id, invoice.client_id, float(amount))
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"Failed to send client notification for payment {payment.id}: {e}", exc_info=True)
return {"success": True, "message": "Payment created successfully", "payment": payment}
def get_invoice_payments(self, invoice_id: int) -> List[Payment]:
"""Get all payments for an invoice"""
return self.payment_repo.get_by_invoice(invoice_id, include_relations=True)
def get_total_paid(self, invoice_id: int) -> Decimal:
"""Get total amount paid for an invoice"""
return self.payment_repo.get_total_for_invoice(invoice_id)
def update_payment(self, payment_id: int, user_id: int, **kwargs) -> Dict[str, Any]:
"""
Update a payment.
Returns:
dict with 'success', 'message', and 'payment' keys
"""
payment = self.payment_repo.get_by_id(payment_id)
if not payment:
return {"success": False, "message": "Payment not found", "error": "not_found"}
# Update fields
for field in ("currency", "method", "reference", "notes", "status"):
if field in kwargs:
setattr(payment, field, kwargs[field])
if "amount" in kwargs:
payment.amount = kwargs["amount"]
if "payment_date" in kwargs:
payment.payment_date = kwargs["payment_date"]
# Recalculate net amount
payment.calculate_net_amount()
# Update invoice payment status if needed
if payment.status == "completed" and payment.invoice:
total_payments = self.payment_repo.get_total_for_invoice(payment.invoice_id)
payment.invoice.amount_paid = total_payments
# Update payment status
if payment.invoice.amount_paid >= payment.invoice.total_amount:
payment.invoice.payment_status = "fully_paid"
elif payment.invoice.amount_paid > 0:
payment.invoice.payment_status = "partially_paid"
else:
payment.invoice.payment_status = "unpaid"
if not safe_commit("update_payment", {"payment_id": payment_id, "user_id": user_id}):
return {
"success": False,
"message": "Could not update payment due to a database error",
"error": "database_error",
}
return {"success": True, "message": "Payment updated successfully", "payment": payment}
def delete_payment(self, payment_id: int, user_id: int) -> Dict[str, Any]:
"""
Delete a payment.
Returns:
dict with 'success' and 'message' keys
"""
payment = self.payment_repo.get_by_id(payment_id)
if not payment:
return {"success": False, "message": "Payment not found", "error": "not_found"}
invoice_id = payment.invoice_id
# Delete payment
db.session.delete(payment)
# Update invoice payment status
if payment.invoice:
total_payments = self.payment_repo.get_total_for_invoice(invoice_id)
payment.invoice.amount_paid = total_payments
# Update payment status
if payment.invoice.amount_paid >= payment.invoice.total_amount:
payment.invoice.payment_status = "fully_paid"
elif payment.invoice.amount_paid > 0:
payment.invoice.payment_status = "partially_paid"
else:
payment.invoice.payment_status = "unpaid"
if not safe_commit("delete_payment", {"payment_id": payment_id, "user_id": user_id}):
return {
"success": False,
"message": "Could not delete payment due to a database error",
"error": "database_error",
}
return {"success": True, "message": "Payment deleted successfully"}