package invoice import ( "fmt" "os" "path/filepath" "strings" "github.com/gofiber/fiber/v2" "github.com/techxcar/backend/internal/auth" "github.com/techxcar/backend/internal/settings" "github.com/techxcar/backend/internal/workorder" "github.com/techxcar/backend/pkg/database" "github.com/techxcar/backend/pkg/pdf" ) const storageRoot = "/app/storage" func RegisterRoutes(app *fiber.App, db *database.DB, secret string) { ro := []fiber.Handler{ auth.RequireAuth(secret), auth.RequireRole("tenant_admin", "manager", "technician"), auth.TenantMiddleware(db), } write := []fiber.Handler{ auth.RequireAuth(secret), auth.RequireRole("tenant_admin", "manager"), auth.TenantMiddleware(db), } app.Get("/api/v1/invoices", append(ro, listInvoicesH())...) app.Post("/api/v1/invoices", append(write, createInvoiceH())...) app.Get("/api/v1/invoices/:id/pdf", append(ro, downloadPDFH())...) } func listInvoicesH() fiber.Handler { return func(c *fiber.Ctx) error { conn := auth.GetConn(c) list, err := ListInvoices(c.Context(), conn) if err != nil { return fiber.NewError(500, "erro ao listar faturas") } if list == nil { list = []*Invoice{} } return c.JSON(fiber.Map{"data": list, "error": nil}) } } type createBody struct { WorkOrderID string `json:"work_order_id"` Type string `json:"type"` // "quote" or "invoice" } func tenantDir(c *fiber.Ctx) string { claims, _ := c.Locals("claims").(*auth.Claims) if claims == nil || claims.TenantID == "" { return "unknown" } return "tenant_" + strings.ReplaceAll(claims.TenantID, "-", "_") } func createInvoiceH() fiber.Handler { return func(c *fiber.Ctx) error { var b createBody if err := c.BodyParser(&b); err != nil { return fiber.NewError(400, "corpo do pedido inválido") } if b.WorkOrderID == "" { return fiber.NewError(400, "work_order_id obrigatório") } if b.Type != "quote" && b.Type != "invoice" { return fiber.NewError(400, "tipo deve ser 'quote' ou 'invoice'") } conn := auth.GetConn(c) detail, err := workorder.GetWorkOrderDetail(c.Context(), conn, b.WorkOrderID) if err != nil { return fiber.NewError(500, "erro ao obter ordem de trabalho") } if detail == nil { return fiber.NewError(404, "ordem de trabalho não encontrada") } sett, err := settings.GetSettings(c.Context(), conn) if err != nil { return fiber.NewError(500, "erro ao obter definições") } var clientName, clientNIF, vehiclePlate string if detail.ClientID != nil { _ = conn.QueryRow(c.Context(), `SELECT name, COALESCE(nif,'') FROM clients WHERE id = $1`, *detail.ClientID).Scan(&clientName, &clientNIF) } if detail.VehicleID != nil { _ = conn.QueryRow(c.Context(), `SELECT plate FROM vehicles WHERE id = $1`, *detail.VehicleID).Scan(&vehiclePlate) } inv, err := CreateInvoice(c.Context(), conn, b.WorkOrderID, b.Type) if err != nil { return fiber.NewError(500, "erro ao criar fatura") } docType := "Orcamento" prefix := "ORC" if b.Type == "invoice" { docType = "Fatura" prefix = "FAT" } outPath := filepath.Join(storageRoot, tenantDir(c), fmt.Sprintf("inv_%s.pdf", inv.ID)) meta := pdf.DocMeta{ CompanyName: sett["company_name"], CompanyNIF: sett["company_nif"], CompanyAddress: sett["company_address"], CompanyIBAN: sett["company_iban"], CompanyPhone: sett["company_phone"], CompanyEmail: sett["company_email"], DocType: docType, DocNumber: fmt.Sprintf("%s/%d/%04d", prefix, inv.IssuedAt.Year(), inv.Number), IssuedAt: inv.IssuedAt.Format("02/01/2006"), ClientName: clientName, ClientNIF: clientNIF, VehiclePlate: vehiclePlate, } lineItems := make([]pdf.LineItem, len(detail.Items)) for i, item := range detail.Items { lineItems[i] = pdf.LineItem{ Description: item.Description, Qty: item.Qty, UnitPrice: item.UnitPrice, DiscountPct: item.DiscountPct, Total: item.Total, } } var staffTotal float64 for _, sh := range detail.StaffHours { staffTotal += sh.Total } if err := pdf.Generate(meta, lineItems, staffTotal, outPath); err != nil { return fiber.NewError(500, "erro ao gerar PDF") } if err := SetPDFPath(c.Context(), conn, inv.ID, outPath); err != nil { return fiber.NewError(500, "erro ao registar caminho do PDF") } inv.PDFPath = outPath if b.Type == "invoice" { if _, err := workorder.TransitionStatus(c.Context(), conn, b.WorkOrderID, "invoiced", ""); err != nil { return fiber.NewError(500, "erro ao atualizar estado da ordem") } } return c.Status(201).JSON(fiber.Map{"data": inv, "error": nil}) } } func downloadPDFH() fiber.Handler { return func(c *fiber.Ctx) error { id := c.Params("id") conn := auth.GetConn(c) inv, err := GetInvoice(c.Context(), conn, id) if err != nil { return fiber.NewError(500, "erro interno") } if inv == nil { return fiber.NewError(404, "fatura não encontrada") } if inv.PDFPath == "" { return fiber.NewError(404, "PDF não disponível") } if _, err := os.Stat(inv.PDFPath); os.IsNotExist(err) { return fiber.NewError(404, "ficheiro PDF não encontrado") } c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="invoice_%d.pdf"`, inv.Number)) return c.SendFile(inv.PDFPath) } }