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