This commit is contained in:
Luciano Milani
2026-07-02 12:47:55 +01:00
commit 5de37bb512
132 changed files with 28495 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
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)
}
}