cmc-sales/go/internal/cmc/handlers/pdf_api.go

524 lines
19 KiB
Go
Raw Normal View History

2026-01-12 03:00:48 -08:00
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"`
Description string `json:"description"`
UnitPrice float64 `json:"unit_price"`
TotalPrice float64 `json:"total_price"`
NetUnitPrice float64 `json:"net_unit_price"`
NetPrice float64 `json:"net_price"`
DiscountPercent float64 `json:"discount_percent"`
DiscountAmountUnit float64 `json:"discount_amount_unit"`
DiscountAmountTotal float64 `json:"discount_amount_total"`
Option int `json:"option"`
HasTextPrices bool `json:"has_text_prices"`
UnitPriceString string `json:"unit_price_string"`
GrossPriceString string `json:"gross_price_string"`
2026-01-12 03:00:48 -08:00
}
// 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
IssueDateString string `json:"issue_date_string"` // Formatted: "12 January 2026"
CurrencySymbol string `json:"currency_symbol"` // e.g. "$"
CurrencyCode string `json:"currency_code"` // e.g. "AUD", "USD"
ShowGST bool `json:"show_gst"`
BillTo string `json:"bill_to"`
ShipTo string `json:"ship_to"`
ShippingDetails string `json:"shipping_details"`
CustomerOrderNumber string `json:"customer_order_number"`
JobTitle string `json:"job_title"`
PaymentTerms string `json:"payment_terms"`
CustomerABN string `json:"customer_abn"`
Subtotal interface{} `json:"subtotal"` // Can be float or "TBA"
GSTAmount interface{} `json:"gst_amount"`
Total interface{} `json:"total"`
LineItems []InvoiceLineItemRequest `json:"line_items"`
OutputDir string `json:"output_dir"` // optional override
2026-01-12 03:00:48 -08:00
}
// GenerateInvoicePDF handles POST /api/pdf/invoice and writes a PDF to disk.
// It returns JSON: {"filename":"<name>.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,
CurrencyCode: req.CurrencyCode,
ShowGST: req.ShowGST,
ShipVia: req.ShipVia,
FOB: req.FOB,
IssueDate: issueDate,
IssueDateString: req.IssueDateString,
EmailTo: req.ContactEmail,
Attention: req.ContactName,
FromName: fmt.Sprintf("%s %s", req.UserFirstName, req.UserLastName),
FromEmail: req.UserEmail,
YourReference: req.YourReference,
BillTo: req.BillTo,
ShipTo: req.ShipTo,
ShippingDetails: req.ShippingDetails,
CustomerOrderNumber: req.CustomerOrderNumber,
JobTitle: req.JobTitle,
PaymentTerms: req.PaymentTerms,
CustomerABN: req.CustomerABN,
Subtotal: req.Subtotal,
GSTAmount: req.GSTAmount,
Total: req.Total,
2026-01-12 03:00:48 -08:00
}
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"`
}
// CountPagesRequest payload for page counting
type CountPagesRequest struct {
FilePath string `json:"file_path"`
}
// CountPages handles POST /go/pdf/count-pages
// Returns JSON: {"page_count": <number>} or {"error": "<message>"}
func CountPages(w http.ResponseWriter, r *http.Request) {
var req CountPagesRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON payload", http.StatusBadRequest)
return
}
if req.FilePath == "" {
http.Error(w, "file_path is required", http.StatusBadRequest)
return
}
// Count pages in the PDF file
pageCount, err := pdf.CountPDFPages(req.FilePath)
if err != nil {
log.Printf("CountPages: error counting pages in %s: %v", req.FilePath, err)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"page_count": 0,
"error": err.Error(),
})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]int{"page_count": pageCount})
}