Inicial
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user