2026-01-26 11:09:40 -05:00

106 lines
4.8 KiB
Python

from typing import cast
import strawberry
from strawberry.types import Info
from core.graphql.pubsub import pubsub
from core.graphql.inputs.invoice import InvoiceInput, InvoiceUpdateInput
from core.graphql.types.invoice import InvoiceType
from core.models.invoice import Invoice
from core.models.enums import InvoiceChoices
from core.graphql.utils import create_object, update_object, delete_object
from core.services.events import publish_invoice_generated, publish_invoice_paid
from core.services.events import EventPublisher
from core.models.enums import EventTypeChoices
@strawberry.type
class Mutation:
@strawberry.mutation(description="Create a new invoice")
async def create_invoice(self, input: InvoiceInput, info: Info) -> InvoiceType:
# Exclude m2m id fields from model constructor
payload = {k: v for k, v in input.__dict__.items() if k not in {"project_ids", "revenue_ids"}}
m2m_data = {
"projects": input.project_ids,
"revenues": input.revenue_ids,
}
instance = await create_object(payload, Invoice, m2m_data)
await pubsub.publish("invoice_created", instance.id)
# Publish event for notifications (invoice creation = invoice generated)
profile = getattr(info.context.request, 'profile', None)
await publish_invoice_generated(
invoice_id=str(instance.id),
triggered_by=profile,
metadata={'customer_id': str(instance.customer_id), 'status': instance.status}
)
return cast(InvoiceType, instance)
@strawberry.mutation(description="Update an existing invoice")
async def update_invoice(self, input: InvoiceUpdateInput, info: Info) -> InvoiceType:
# Get old invoice to check for status changes
from channels.db import database_sync_to_async
old_invoice = await database_sync_to_async(Invoice.objects.get)(pk=input.id.node_id)
old_status = old_invoice.status
# Keep id and non-m2m fields; drop m2m *_ids from the update payload
payload = {k: v for k, v in input.__dict__.items() if k not in {"project_ids", "revenue_ids"}}
m2m_data = {
"projects": getattr(input, "project_ids", None),
"revenues": getattr(input, "revenue_ids", None),
}
instance = await update_object(payload, Invoice, m2m_data)
await pubsub.publish("invoice_updated", instance.id)
# Publish events for notifications
profile = getattr(info.context.request, 'profile', None)
# Check if status changed
if hasattr(input, 'status') and input.status and input.status != old_status:
# Publish status change event
await EventPublisher.publish(
event_type=EventTypeChoices.INVOICE_SENT if input.status == InvoiceChoices.SENT else
EventTypeChoices.INVOICE_PAID if input.status == InvoiceChoices.PAID else
EventTypeChoices.INVOICE_OVERDUE if input.status == InvoiceChoices.OVERDUE else
EventTypeChoices.INVOICE_CANCELLED if input.status == InvoiceChoices.CANCELLED else None,
entity_type='Invoice',
entity_id=str(instance.id),
triggered_by=profile,
metadata={'old_status': old_status, 'new_status': instance.status, 'customer_id': str(instance.customer_id)}
)
# Special handling for paid invoices
if instance.status == InvoiceChoices.PAID:
await publish_invoice_paid(
invoice_id=str(instance.id),
triggered_by=profile,
metadata={'customer_id': str(instance.customer_id), 'amount': str(instance.amount)}
)
return cast(InvoiceType, instance)
@strawberry.mutation(description="Delete an existing invoice")
async def delete_invoice(self, id: strawberry.ID, info: Info) -> strawberry.ID:
# Get invoice before deletion to access customer_id for event
from channels.db import database_sync_to_async
from core.graphql.utils import _decode_global_id
pk = _decode_global_id(id)
invoice = await database_sync_to_async(Invoice.objects.get)(pk=pk)
customer_id = str(invoice.customer_id)
instance = await delete_object(id, Invoice)
if not instance:
raise ValueError(f"Invoice with ID {id} does not exist")
await pubsub.publish("invoice_deleted", id)
# Publish event for notifications (deletion treated as cancellation)
profile = getattr(info.context.request, 'profile', None)
await EventPublisher.publish(
event_type=EventTypeChoices.INVOICE_CANCELLED,
entity_type='Invoice',
entity_id=str(id),
triggered_by=profile,
metadata={'customer_id': customer_id, 'action': 'deleted'}
)
return id