package documents
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"html"
"log"
"net/http"
"os"
"strings"
"time"
"code.springupsoftware.com/cmc/cmc-sales/internal/cmc/db"
pdf "code.springupsoftware.com/cmc/cmc-sales/internal/cmc/documents"
)
// DocumentHandler handles document PDF generation with database integration
type DocumentHandler struct {
queries *db.Queries
}
// NewDocumentHandler creates a new DocumentHandler
func NewDocumentHandler(queries *db.Queries) *DocumentHandler {
return &DocumentHandler{
queries: queries,
}
}
// escapeToHTML converts plain text to HTML with newlines as
func escapeToHTML(s string) string {
s = html.EscapeString(s)
s = strings.ReplaceAll(s, "\n", "
")
return s
}
// 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"`
IsHTML bool `json:"is_html"` // Flag to indicate description contains HTML
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"`
}
// InvoicePDFRequest is the expected payload from the PHP app.
type InvoicePDFRequest struct {
DocumentID int32 `json:"document_id"`
InvoiceNumber string `json:"invoice_number"` // e.g. "INV-001234"
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
}
// GenerateInvoicePDF handles POST /api/pdf/invoice and writes a PDF to disk.
// It returns JSON: {"filename":".pdf"}
// GenerateInvoicePDF generates invoice using HTML template and chromedp
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, CmcReference: req.InvoiceNumber}
inv := &db.Invoice{Title: req.InvoiceTitle}
cust := &db.Customer{Name: req.CustomerName}
log.Printf("GenerateInvoicePDF: Setting invoice number to: %s", req.InvoiceNumber)
lineItems := make([]db.GetLineItemsTableRow, len(req.LineItems))
for i, li := range req.LineItems {
// Escape description if it's not HTML
desc := li.Description
if !li.IsHTML {
// Escape plain text to HTML entities
desc = escapeToHTML(desc)
}
// Calculate the final price after discount
finalPrice := li.TotalPrice - li.DiscountAmountTotal
lineItems[i] = db.GetLineItemsTableRow{
ItemNumber: li.ItemNumber,
Quantity: li.Quantity,
Title: li.Title,
Description: desc,
GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true},
GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", finalPrice), Valid: true},
DiscountAmountTotal: sql.NullString{String: fmt.Sprintf("%.2f", li.DiscountAmountTotal), Valid: li.DiscountAmountTotal > 0},
HasTextPrices: li.HasTextPrices,
UnitPriceString: sql.NullString{String: li.UnitPriceString, Valid: li.UnitPriceString != ""},
GrossPriceString: sql.NullString{String: li.GrossPriceString, Valid: li.GrossPriceString != ""},
}
}
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,
}
// Use HTML generator instead of gofpdf
htmlGen := pdf.NewHTMLDocumentGenerator(outputDir)
filename, err := htmlGen.GenerateInvoicePDF(data)
if err != nil {
log.Printf("GenerateInvoicePDF: failed to generate PDF: %v", err)
http.Error(w, fmt.Sprintf("failed to generate PDF: %v", err), 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
CreatedDateString string `json:"created_date_string"` // j M Y format
DateIssued string `json:"date_issued"`
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"`
CurrencyCode string `json:"currency_code"`
ShowGST bool `json:"show_gst"`
CommercialComments string `json:"commercial_comments"`
DeliveryTime string `json:"delivery_time"`
PaymentTerms string `json:"payment_terms"`
DaysValid int32 `json:"daysValid"`
DeliveryPoint string `json:"delivery_point"`
ExchangeRate string `json:"exchange_rate"`
CustomsDuty string `json:"customs_duty"`
GSTPhrase string `json:"gst_phrase"`
SalesEngineer string `json:"sales_engineer"`
BillTo string `json:"bill_to"`
ShipTo string `json:"ship_to"`
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 {
log.Printf("GenerateQuotePDF: JSON decode error: %v", err)
http.Error(w, fmt.Sprintf("invalid JSON payload: %v", err), http.StatusBadRequest)
return
}
log.Printf("GenerateQuotePDF: Received request - DocumentID=%d, CmcReference='%s', Revision=%d, CustomerName='%s'",
req.DocumentID, req.CmcReference, req.Revision, req.CustomerName)
if req.CmcReference == "" || req.CustomerName == "" {
log.Printf("GenerateQuotePDF: missing required fields - cmc_reference='%s', customer_name='%s'", 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,
Description: li.Description,
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,
CurrencyCode: req.CurrencyCode,
ShowGST: req.ShowGST,
CommercialComments: req.CommercialComments,
DeliveryTime: req.DeliveryTime,
PaymentTerms: req.PaymentTerms,
DaysValid: int(req.DaysValid),
DeliveryPoint: req.DeliveryPoint,
ExchangeRate: req.ExchangeRate,
CustomsDuty: req.CustomsDuty,
GSTPhrase: req.GSTPhrase,
SalesEngineer: req.SalesEngineer,
BillTo: req.BillTo,
ShipTo: req.ShipTo,
IssueDateString: req.CreatedDateString,
Pages: req.Pages,
}
// Use HTML generator
htmlGen := pdf.NewHTMLDocumentGenerator(outputDir)
filename, err := htmlGen.GenerateQuotePDF(data)
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"`
UserID int32 `json:"user_id"`
Title string `json:"title"`
IssueDate string `json:"issue_date"` // YYYY-MM-DD
IssueDateString string `json:"issue_date_string"` // formatted date
PrincipleName string `json:"principle_name"`
PrincipleReference string `json:"principle_reference"`
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"`
CurrencyCode string `json:"currency_code"`
ShowGST bool `json:"show_gst"`
Subtotal float64 `json:"subtotal"`
GSTAmount float64 `json:"gst_amount"`
Total float64 `json:"total"`
LineItems []PurchaseOrderLineItemRequest `json:"line_items"`
OutputDir string `json:"output_dir"`
}
// GeneratePurchaseOrderPDF handles POST /go/document/generate/purchase-order
func (h *DocumentHandler) 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 {
// Keep description as-is to support HTML rendering
lineItems[i] = db.GetLineItemsTableRow{
ItemNumber: li.ItemNumber,
Quantity: li.Quantity,
Title: li.Title,
Description: li.Description,
NetUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.NetUnitPrice), Valid: true},
NetPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.NetPrice), Valid: true},
DiscountAmountTotal: sql.NullString{String: fmt.Sprintf("%.2f", li.DiscountAmountTotal), Valid: true},
Option: li.Option != 0,
HasTextPrices: li.HasTextPrices,
}
}
data := &pdf.PurchaseOrderPDFData{
Document: doc,
PurchaseOrder: po,
Principle: principle,
LineItems: lineItems,
CurrencySymbol: req.CurrencySymbol,
CurrencyCode: req.CurrencyCode,
ShowGST: req.ShowGST,
Subtotal: req.Subtotal,
GSTAmount: req.GSTAmount,
Total: req.Total,
IssueDateString: req.IssueDateString,
}
// Use HTML generator
gen := pdf.NewHTMLDocumentGenerator(outputDir)
filename, err := gen.GeneratePurchaseOrderPDF(data)
if err != nil {
log.Printf("GeneratePurchaseOrderPDF: failed to generate PDF: %v", err)
http.Error(w, "failed to generate PDF", http.StatusInternalServerError)
return
}
// Update the document record with PDF generation info
ctx := context.Background()
updateParams := db.UpdateDocumentParams{
ID: req.DocumentID,
PdfFilename: filename,
PdfCreatedAt: time.Now(),
PdfCreatedByUserID: req.UserID,
Type: db.DocumentsTypePurchaseOrder,
UserID: req.UserID,
DocPageCount: 0, // TODO: extract from PDF
CmcReference: "",
ShippingDetails: sql.NullString{},
Revision: 0,
BillTo: sql.NullString{},
ShipTo: sql.NullString{},
EmailSentAt: time.Time{},
EmailSentByUserID: 0,
}
if err := h.queries.UpdateDocument(ctx, updateParams); err != nil {
log.Printf("GeneratePurchaseOrderPDF: failed to update document: %v", err)
// Don't fail the request if update fails, just log it
}
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{
PackingList: doc,
Customer: cust,
JobTitle: req.JobTitle,
IssueDateString: req.IssueDateString,
CustomerOrderNumber: req.CustomerOrderNumber,
ShipTo: req.ShipTo,
LineItems: lineItems,
}
// Use HTML generator
gen := pdf.NewHTMLDocumentGenerator(outputDir)
filename, err := gen.GeneratePackingListPDF(data)
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, CmcReference: req.CmcReference}
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,
Description: li.Description,
GrossUnitPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.UnitPrice), Valid: true},
GrossPrice: sql.NullString{String: fmt.Sprintf("%.2f", li.TotalPrice), Valid: true},
DiscountAmountTotal: sql.NullString{String: fmt.Sprintf("%.2f", li.DiscountAmountTotal), Valid: li.DiscountAmountTotal > 0},
HasTextPrices: li.HasTextPrices,
UnitPriceString: sql.NullString{String: li.UnitPriceString, Valid: li.UnitPriceString != ""},
GrossPriceString: sql.NullString{String: li.GrossPriceString, Valid: li.GrossPriceString != ""},
}
}
data := &pdf.OrderAckPDFData{
OrderAcknowledgement: doc,
Customer: cust,
CmcReference: req.CmcReference,
EmailTo: req.EmailTo,
Attention: req.Attention,
IssueDateString: req.IssueDateString,
YourReference: req.YourReference,
JobTitle: req.JobTitle,
CustomerOrderNumber: req.CustomerOrderNumber,
CustomerABN: req.CustomerABN,
BillTo: req.BillTo,
ShipTo: req.ShipTo,
ShipVia: req.ShipVia,
FOB: req.FOB,
PaymentTerms: req.PaymentTerms,
FreightDetails: req.FreightDetails,
CurrencyCode: req.CurrencyCode,
CurrencySymbol: req.CurrencySymbol,
ShowGST: req.ShowGST,
LineItems: lineItems,
}
// Use HTML generator
gen := pdf.NewHTMLDocumentGenerator(outputDir)
filename, err := gen.GenerateOrderAckPDF(data)
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"`
Title string `json:"title"`
CustomerName string `json:"customer_name"`
CustomerOrderNumber string `json:"customer_order_number"`
JobTitle string `json:"job_title"`
IssueDate string `json:"issue_date"` // YYYY-MM-DD
IssueDateString string `json:"issue_date_string"` // formatted date
ShipTo string `json:"ship_to"`
ShipVia string `json:"ship_via"`
FOB string `json:"fob"`
CurrencySymbol string `json:"currency_symbol"`
CurrencyCode string `json:"currency_code"`
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"`
Title string `json:"title"`
CustomerName string `json:"customer_name"`
CmcReference string `json:"cmc_reference"`
CustomerOrderNumber string `json:"customer_order_number"`
CustomerABN string `json:"customer_abn"`
EmailTo string `json:"email_to"`
Attention string `json:"attention"`
YourReference string `json:"your_reference"`
JobTitle string `json:"job_title"`
IssueDate string `json:"issue_date"` // YYYY-MM-DD
IssueDateString string `json:"issue_date_string"` // formatted date
BillTo string `json:"bill_to"`
ShipTo string `json:"ship_to"`
ShipVia string `json:"ship_via"`
FOB string `json:"fob"`
PaymentTerms string `json:"payment_terms"`
FreightDetails string `json:"freight_details"`
EstimatedDelivery string `json:"estimated_delivery"`
CurrencySymbol string `json:"currency_symbol"`
CurrencyCode string `json:"currency_code"`
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": } or {"error": ""}
func CountPages(w http.ResponseWriter, r *http.Request) {
var req CountPagesRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
log.Printf("CountPages: JSON decode error: %v", err)
http.Error(w, "invalid JSON payload", http.StatusBadRequest)
return
}
if req.FilePath == "" {
log.Printf("CountPages: file_path is required")
http.Error(w, "file_path is required", http.StatusBadRequest)
return
}
// Normalize path: remove double slashes
normalizedPath := strings.ReplaceAll(req.FilePath, "//", "/")
log.Printf("CountPages: Attempting to count pages for file: %s", normalizedPath)
// Count pages in the PDF file
pageCount, err := pdf.CountPDFPages(normalizedPath)
if err != nil {
log.Printf("CountPages: error counting pages in %s: %v", normalizedPath, 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
}
log.Printf("CountPages: Successfully counted %d pages in %s", pageCount, normalizedPath)
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]int{"page_count": pageCount})
}
// Handler methods that delegate to standalone functions
func (h *DocumentHandler) GenerateInvoicePDF(w http.ResponseWriter, r *http.Request) {
GenerateInvoicePDF(w, r)
}
func (h *DocumentHandler) GenerateQuotePDF(w http.ResponseWriter, r *http.Request) {
GenerateQuotePDF(w, r)
}
func (h *DocumentHandler) GeneratePackingListPDF(w http.ResponseWriter, r *http.Request) {
GeneratePackingListPDF(w, r)
}
func (h *DocumentHandler) GenerateOrderAckPDF(w http.ResponseWriter, r *http.Request) {
GenerateOrderAckPDF(w, r)
}
func (h *DocumentHandler) CountPages(w http.ResponseWriter, r *http.Request) {
CountPages(w, r)
}