package handlers import ( "database/sql" "encoding/json" "fmt" "log" "net/http" "os" "time" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db" "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/pdf" ) // InvoiceLineItemRequest is the JSON shape for a single line item. type InvoiceLineItemRequest struct { ItemNumber string `json:"item_number"` Quantity string `json:"quantity"` Title string `json:"title"` UnitPrice float64 `json:"unit_price"` TotalPrice float64 `json:"total_price"` } // InvoicePDFRequest is the expected payload from the PHP app. type InvoicePDFRequest struct { DocumentID int32 `json:"document_id"` InvoiceTitle string `json:"invoice_title"` CustomerName string `json:"customer_name"` ContactEmail string `json:"contact_email"` ContactName string `json:"contact_name"` UserFirstName string `json:"user_first_name"` UserLastName string `json:"user_last_name"` UserEmail string `json:"user_email"` YourReference string `json:"your_reference"` ShipVia string `json:"ship_via"` FOB string `json:"fob"` IssueDate string `json:"issue_date"` // ISO date: 2006-01-02 CurrencySymbol string `json:"currency_symbol"` // e.g. "$" ShowGST bool `json:"show_gst"` LineItems []InvoiceLineItemRequest `json:"line_items"` OutputDir string `json:"output_dir"` // optional override } // GenerateInvoicePDF handles POST /api/pdf/invoice and writes a PDF to disk. // It returns JSON: {"filename":".pdf"} func GenerateInvoicePDF(w http.ResponseWriter, r *http.Request) { var req InvoicePDFRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON payload", http.StatusBadRequest) return } if req.InvoiceTitle == "" || req.CustomerName == "" { http.Error(w, "invoice_title and customer_name are required", http.StatusBadRequest) return } issueDate := time.Now() if req.IssueDate != "" { if parsed, err := time.Parse("2006-01-02", req.IssueDate); err == nil { issueDate = parsed } } outputDir := req.OutputDir if outputDir == "" { outputDir = os.Getenv("PDF_OUTPUT_DIR") } if outputDir == "" { outputDir = "../php/app/webroot/pdf" } if err := os.MkdirAll(outputDir, 0755); err != nil { log.Printf("GenerateInvoicePDF: failed to create output dir: %v", err) http.Error(w, "failed to prepare output directory", http.StatusInternalServerError) return } // Map request into the existing PDF generation types. doc := &db.Document{ID: req.DocumentID} inv := &db.Invoice{Title: req.InvoiceTitle} cust := &db.Customer{Name: req.CustomerName} lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems)) for i, li := range req.LineItems { lineItems[i] = db.GetLineItemsTableRow{ ItemNumber: li.ItemNumber, Quantity: li.Quantity, Title: li.Title, GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true}, } } data := &pdf.InvoicePDFData{ Document: doc, Invoice: inv, Customer: cust, LineItems: lineItems, CurrencySymbol: req.CurrencySymbol, ShowGST: req.ShowGST, ShipVia: req.ShipVia, FOB: req.FOB, IssueDate: issueDate, EmailTo: req.ContactEmail, Attention: req.ContactName, FromName: fmt.Sprintf("%s %s", req.UserFirstName, req.UserLastName), FromEmail: req.UserEmail, YourReference: req.YourReference, } filename, err := pdf.GenerateInvoicePDF(data, outputDir) if err != nil { log.Printf("GenerateInvoicePDF: failed to generate PDF: %v", err) http.Error(w, "failed to generate PDF", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"filename": filename}) } // QuoteLineItemRequest reuses the invoice item shape type QuoteLineItemRequest = InvoiceLineItemRequest // QuotePDFRequest payload from PHP for quotes type QuotePDFRequest struct { DocumentID int32 `json:"document_id"` CmcReference string `json:"cmc_reference"` Revision int32 `json:"revision"` CreatedDate string `json:"created_date"` // YYYY-MM-DD CustomerName string `json:"customer_name"` ContactEmail string `json:"contact_email"` ContactName string `json:"contact_name"` UserFirstName string `json:"user_first_name"` UserLastName string `json:"user_last_name"` UserEmail string `json:"user_email"` CurrencySymbol string `json:"currency_symbol"` ShowGST bool `json:"show_gst"` CommercialComments string `json:"commercial_comments"` LineItems []QuoteLineItemRequest `json:"line_items"` Pages []string `json:"pages"` OutputDir string `json:"output_dir"` } // GenerateQuotePDF handles POST /go/pdf/generate-quote func GenerateQuotePDF(w http.ResponseWriter, r *http.Request) { var req QuotePDFRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON payload", http.StatusBadRequest) return } if req.CmcReference == "" || req.CustomerName == "" { http.Error(w, "cmc_reference and customer_name are required", http.StatusBadRequest) return } created := time.Now() if req.CreatedDate != "" { if parsed, err := time.Parse("2006-01-02", req.CreatedDate); err == nil { created = parsed } } outputDir := req.OutputDir if outputDir == "" { outputDir = os.Getenv("PDF_OUTPUT_DIR") } if outputDir == "" { outputDir = "../php/app/webroot/pdf" } if err := os.MkdirAll(outputDir, 0755); err != nil { log.Printf("GenerateQuotePDF: failed to create output dir: %v", err) http.Error(w, "failed to prepare output directory", http.StatusInternalServerError) return } // Map request into PDF data doc := &db.Document{ID: req.DocumentID, CmcReference: req.CmcReference, Revision: req.Revision, Created: created} cust := &db.Customer{Name: req.CustomerName} user := &db.GetUserRow{FirstName: req.UserFirstName, LastName: req.UserLastName, Email: req.UserEmail} lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems)) for i, li := range req.LineItems { lineItems[i] = db.GetLineItemsTableRow{ ItemNumber: li.ItemNumber, Quantity: li.Quantity, Title: li.Title, GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true}, } } data := &pdf.QuotePDFData{ Document: doc, Customer: cust, EmailTo: req.ContactEmail, Attention: req.ContactName, User: user, LineItems: lineItems, CurrencySymbol: req.CurrencySymbol, ShowGST: req.ShowGST, CommercialComments: req.CommercialComments, Pages: req.Pages, } filename, err := pdf.GenerateQuotePDF(data, outputDir) if err != nil { log.Printf("GenerateQuotePDF: failed to generate PDF: %v", err) http.Error(w, "failed to generate PDF", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"filename": filename}) } // PurchaseOrderLineItemRequest reuses the invoice item shape type PurchaseOrderLineItemRequest = InvoiceLineItemRequest // PurchaseOrderPDFRequest payload from PHP for POs type PurchaseOrderPDFRequest struct { DocumentID int32 `json:"document_id"` Title string `json:"title"` PrincipleName string `json:"principle_name"` PrincipleReference string `json:"principle_reference"` IssueDate string `json:"issue_date"` // YYYY-MM-DD OrderedFrom string `json:"ordered_from"` DispatchBy string `json:"dispatch_by"` DeliverTo string `json:"deliver_to"` ShippingInstructions string `json:"shipping_instructions"` CurrencySymbol string `json:"currency_symbol"` ShowGST bool `json:"show_gst"` LineItems []PurchaseOrderLineItemRequest `json:"line_items"` OutputDir string `json:"output_dir"` } // GeneratePurchaseOrderPDF handles POST /go/pdf/generate-po func GeneratePurchaseOrderPDF(w http.ResponseWriter, r *http.Request) { var req PurchaseOrderPDFRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON payload", http.StatusBadRequest) return } if req.Title == "" || req.PrincipleName == "" { http.Error(w, "title and principle_name are required", http.StatusBadRequest) return } issueDate := time.Now() if req.IssueDate != "" { if parsed, err := time.Parse("2006-01-02", req.IssueDate); err == nil { issueDate = parsed } } outputDir := req.OutputDir if outputDir == "" { outputDir = os.Getenv("PDF_OUTPUT_DIR") } if outputDir == "" { outputDir = "../php/app/webroot/pdf" } if err := os.MkdirAll(outputDir, 0755); err != nil { log.Printf("GeneratePurchaseOrderPDF: failed to create output dir: %v", err) http.Error(w, "failed to prepare output directory", http.StatusInternalServerError) return } doc := &db.Document{ID: req.DocumentID} po := &db.PurchaseOrder{ Title: req.Title, PrincipleReference: req.PrincipleReference, IssueDate: issueDate, OrderedFrom: req.OrderedFrom, DispatchBy: req.DispatchBy, DeliverTo: req.DeliverTo, ShippingInstructions: req.ShippingInstructions, } principle := &db.Principle{Name: req.PrincipleName} lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems)) for i, li := range req.LineItems { lineItems[i] = db.GetLineItemsTableRow{ ItemNumber: li.ItemNumber, Quantity: li.Quantity, Title: li.Title, GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true}, } } data := &pdf.PurchaseOrderPDFData{ Document: doc, PurchaseOrder: po, Principle: principle, LineItems: lineItems, CurrencySymbol: req.CurrencySymbol, ShowGST: req.ShowGST, } filename, err := pdf.GeneratePurchaseOrderPDF(data, outputDir) if err != nil { log.Printf("GeneratePurchaseOrderPDF: failed to generate PDF: %v", err) http.Error(w, "failed to generate PDF", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"filename": filename}) } // GeneratePackingListPDF handles POST /go/pdf/generate-packinglist func GeneratePackingListPDF(w http.ResponseWriter, r *http.Request) { var req PackingListPDFRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON payload", http.StatusBadRequest) return } if req.CustomerName == "" { http.Error(w, "customer_name is required", http.StatusBadRequest) return } outputDir := req.OutputDir if outputDir == "" { outputDir = os.Getenv("PDF_OUTPUT_DIR") } if outputDir == "" { outputDir = "../php/app/webroot/pdf" } if err := os.MkdirAll(outputDir, 0755); err != nil { log.Printf("GeneratePackingListPDF: failed to create output dir: %v", err) http.Error(w, "failed to prepare output directory", http.StatusInternalServerError) return } // Reuse the invoice generator structure but label as PACKING LIST via DetailsBox // Build minimal data shape doc := &db.Document{ID: req.DocumentID} cust := &db.Customer{Name: req.CustomerName} lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems)) for i, li := range req.LineItems { lineItems[i] = db.GetLineItemsTableRow{ ItemNumber: li.ItemNumber, Quantity: li.Quantity, Title: li.Title, GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true}, } } data := &pdf.PackingListPDFData{ Document: doc, Customer: cust, LineItems: lineItems, CurrencySymbol: req.CurrencySymbol, ShowGST: req.ShowGST, } filename, err := pdf.GeneratePackingListPDF(data, outputDir) if err != nil { log.Printf("GeneratePackingListPDF: failed to generate PDF: %v", err) http.Error(w, "failed to generate PDF", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"filename": filename}) } // GenerateOrderAckPDF handles POST /go/pdf/generate-orderack func GenerateOrderAckPDF(w http.ResponseWriter, r *http.Request) { var req OrderAckPDFRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON payload", http.StatusBadRequest) return } if req.CustomerName == "" { http.Error(w, "customer_name is required", http.StatusBadRequest) return } outputDir := req.OutputDir if outputDir == "" { outputDir = os.Getenv("PDF_OUTPUT_DIR") } if outputDir == "" { outputDir = "../php/app/webroot/pdf" } if err := os.MkdirAll(outputDir, 0755); err != nil { log.Printf("GenerateOrderAckPDF: failed to create output dir: %v", err) http.Error(w, "failed to prepare output directory", http.StatusInternalServerError) return } doc := &db.Document{ID: req.DocumentID} cust := &db.Customer{Name: req.CustomerName} lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems)) for i, li := range req.LineItems { lineItems[i] = db.GetLineItemsTableRow{ ItemNumber: li.ItemNumber, Quantity: li.Quantity, Title: li.Title, GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true}, GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true}, } } data := &pdf.OrderAckPDFData{ Document: doc, Customer: cust, LineItems: lineItems, CurrencySymbol: req.CurrencySymbol, ShowGST: req.ShowGST, } filename, err := pdf.GenerateOrderAckPDF(data, outputDir) if err != nil { log.Printf("GenerateOrderAckPDF: failed to generate PDF: %v", err) http.Error(w, "failed to generate PDF", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]string{"filename": filename}) } // PackingListLineItemRequest reuses the invoice item shape type PackingListLineItemRequest = InvoiceLineItemRequest // PackingListPDFRequest payload type PackingListPDFRequest struct { DocumentID int32 `json:"document_id"` CustomerName string `json:"customer_name"` CurrencySymbol string `json:"currency_symbol"` ShowGST bool `json:"show_gst"` LineItems []PackingListLineItemRequest `json:"line_items"` OutputDir string `json:"output_dir"` } // OrderAckLineItemRequest reuses the invoice item shape type OrderAckLineItemRequest = InvoiceLineItemRequest // OrderAckPDFRequest payload type OrderAckPDFRequest struct { DocumentID int32 `json:"document_id"` CustomerName string `json:"customer_name"` CurrencySymbol string `json:"currency_symbol"` ShowGST bool `json:"show_gst"` LineItems []OrderAckLineItemRequest `json:"line_items"` OutputDir string `json:"output_dir"` }