package api import ( "bytes" "encoding/json" "fmt" "io" "log" "mime/multipart" "net/http" "strconv" "strings" ) // ServiceTrade attachment purpose constants with file type restrictions const ( // AttachmentPurposeJobPaperwork - PDF documents accepted AttachmentPurposeJobPaperwork = 1 // AttachmentPurposeJobVendorBill - PDF documents accepted AttachmentPurposeJobVendorBill = 2 // AttachmentPurposeJobPicture - Only image files: *.gif, *.jpeg, *.jpg, *.png, *.tif, *.tiff AttachmentPurposeJobPicture = 3 // AttachmentPurposeDeficiencyRepairProposal - PDF documents accepted AttachmentPurposeDeficiencyRepairProposal = 5 // AttachmentPurposeGenericAttachment - PDF documents accepted AttachmentPurposeGenericAttachment = 7 // AttachmentPurposeAvatarImage - Only image files: *.gif, *.jpeg, *.jpg, *.png, *.tif, *.tiff AttachmentPurposeAvatarImage = 8 // AttachmentPurposeImport - Only text files: *.csv, *.txt AttachmentPurposeImport = 9 // AttachmentPurposeBlankPaperwork - PDF documents accepted AttachmentPurposeBlankPaperwork = 10 // AttachmentPurposeWorkAcknowledgement - Only JSON files: *.json AttachmentPurposeWorkAcknowledgement = 11 // AttachmentPurposeLogo - Only image files: *.gif, *.jpeg, *.jpg, *.png, *.tif, *.tiff AttachmentPurposeLogo = 12 // AttachmentPurposeJobInvoice - PDF documents accepted AttachmentPurposeJobInvoice = 14 ) // UploadAttachment uploads a file as an attachment to a job func (s *Session) UploadAttachment(jobID, filename, purpose string, fileContent []byte) (map[string]interface{}, error) { url := fmt.Sprintf("%s/attachment", BaseURL) // Create a buffer to hold the form data var b bytes.Buffer w := multipart.NewWriter(&b) // Log received values log.Printf("Uploading attachment to job ID: %s", jobID) log.Printf("Filename: %s", filename) log.Printf("Purpose value: '%s'", purpose) log.Printf("File content length: %d bytes", len(fileContent)) // The ServiceTrade API expects the purpose ID as an integer purposeStr := strings.TrimSpace(purpose) // Try to parse the purpose as an integer, removing any leading zeros first purposeStr = strings.TrimLeft(purposeStr, "0") if purposeStr == "" { purposeStr = "0" // If only zeros were provided } purposeInt, err := strconv.Atoi(purposeStr) if err != nil { return nil, fmt.Errorf("invalid purpose value '%s': must be a valid integer: %v", purpose, err) } log.Printf("Using purpose value: %d for job: %s", purposeInt, jobID) // Add the purposeId (attachment type) as an integer // NOTE: The API expects "purposeId", not "purpose" if err := w.WriteField("purposeId", fmt.Sprintf("%d", purposeInt)); err != nil { return nil, fmt.Errorf("error writing purposeId field: %v", err) } // Add the entityType (3 for Job) and entityId (jobID) if err := w.WriteField("entityType", "3"); err != nil { // 3 = Job return nil, fmt.Errorf("error writing entityType field: %v", err) } if err := w.WriteField("entityId", jobID); err != nil { return nil, fmt.Errorf("error writing entityId field: %v", err) } // Ensure we have a file with content to upload if len(fileContent) == 0 { return nil, fmt.Errorf("no file content provided for upload") } // Add a description field with the filename for better identification if err := w.WriteField("description", filename); err != nil { return nil, fmt.Errorf("error writing description field: %v", err) } // Add the file - make sure we use the real filename with extension for content-type detection // The API requires the file extension to determine the content type if !strings.Contains(filename, ".") { return nil, fmt.Errorf("filename must include an extension (e.g. .pdf, .docx) for API content type detection") } fw, err := w.CreateFormFile("uploadedFile", filename) if err != nil { return nil, fmt.Errorf("error creating form file: %v", err) } bytesWritten, err := io.Copy(fw, bytes.NewReader(fileContent)) if err != nil { return nil, fmt.Errorf("error copying file content: %v", err) } log.Printf("Wrote %d bytes of file content to the form", bytesWritten) // Close the writer if err := w.Close(); err != nil { return nil, fmt.Errorf("error closing multipart writer: %v", err) } // Create the request req, err := http.NewRequest("POST", url, &b) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } // Set headers req.Header.Set("Content-Type", w.FormDataContentType()) req.Header.Set("Cookie", s.Cookie) // Debug information log.Printf("Sending request to: %s", url) log.Printf("Content-Type: %s", w.FormDataContentType()) log.Printf("Request body fields: purposeId=%d, entityType=3, entityId=%s, filename=%s", purposeInt, jobID, filename) // Send the request resp, err := s.Client.Do(req) if err != nil { return nil, fmt.Errorf("error sending request: %v", err) } defer resp.Body.Close() // Read the response body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response body: %v", err) } // Check for errors if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { log.Printf("API error response: %s - %s", resp.Status, string(body)) return nil, fmt.Errorf("API returned error: %s - %s", resp.Status, string(body)) } // Parse the response var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("error parsing response: %v", err) } log.Printf("Successfully uploaded attachment %s to job %s", filename, jobID) return result, nil } // GetAttachmentInfo gets information about a specific attachment func (s *Session) GetAttachmentInfo(attachmentID string) (map[string]interface{}, error) { url := fmt.Sprintf("%s/attachment/%s", BaseURL, attachmentID) // Create the request req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } // Set headers req.Header.Set("Cookie", s.Cookie) // Send the request resp, err := s.Client.Do(req) if err != nil { return nil, fmt.Errorf("error sending request: %v", err) } defer resp.Body.Close() // Read the response body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response body: %v", err) } // Check for errors if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("API returned error: %s - %s", resp.Status, string(body)) } // Parse the response var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("error parsing response: %v", err) } return result, nil } // DeleteAttachment deletes an attachment func (s *Session) DeleteAttachment(attachmentID string) error { url := fmt.Sprintf("%s/attachment/%s", BaseURL, attachmentID) // Create the request req, err := http.NewRequest("DELETE", url, nil) if err != nil { return fmt.Errorf("error creating request: %v", err) } // Set headers req.Header.Set("Cookie", s.Cookie) // Send the request resp, err := s.Client.Do(req) if err != nil { return fmt.Errorf("error sending request: %v", err) } defer resp.Body.Close() // Check for errors if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("API returned error: %s - %s", resp.Status, string(body)) } return nil } // GetJobAttachments retrieves all attachments for a given job ID func (s *Session) GetJobAttachments(jobID string) ([]map[string]interface{}, error) { log.Printf("GetJobAttachments: Fetching attachments for job %s", jobID) url := fmt.Sprintf("%s/job/%s/paperwork", BaseURL, jobID) req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, fmt.Errorf("error creating request: %v", err) } // Add authorization header req.Header.Set("Cookie", s.Cookie) log.Printf("GetJobAttachments: Authorization cookie length: %d", len(s.Cookie)) // Send the request resp, err := s.Client.Do(req) if err != nil { return nil, fmt.Errorf("error sending request: %v", err) } defer resp.Body.Close() // Read the full response body for complete error reporting body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response body: %v", err) } // Check for various error responses if resp.StatusCode != http.StatusOK { // Check for authentication-related errors if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { log.Printf("GetJobAttachments: Authentication error (status %d) for job %s", resp.StatusCode, jobID) return nil, fmt.Errorf("authentication error: %s - %s", resp.Status, string(body)) } // Check for job not found if resp.StatusCode == http.StatusNotFound { log.Printf("GetJobAttachments: Job %s not found (status 404)", jobID) return nil, fmt.Errorf("job not found: %s", jobID) } // Generic error log.Printf("GetJobAttachments: API error (status %d) for job %s: %s", resp.StatusCode, jobID, string(body)) return nil, fmt.Errorf("API returned error: %d %s - %s", resp.StatusCode, resp.Status, string(body)) } // Parse the response - try multiple formats var result struct { Attachments []map[string]interface{} `json:"objects"` } // Try to parse as standard format first if err := json.Unmarshal(body, &result); err != nil { // If standard parse fails, try parsing as raw map log.Printf("GetJobAttachments: Standard JSON parsing failed, trying alternative formats for job %s: %v", jobID, err) var mapResult map[string]interface{} if err := json.Unmarshal(body, &mapResult); err != nil { return nil, fmt.Errorf("error parsing response: %v", err) } // Try to extract from various places in the map attachments := make([]map[string]interface{}, 0) // Check for objects array if objects, ok := mapResult["objects"].([]interface{}); ok { log.Printf("GetJobAttachments: Found %d attachments in 'objects' field for job %s", len(objects), jobID) for _, obj := range objects { if attachment, ok := obj.(map[string]interface{}); ok { attachments = append(attachments, attachment) } } return attachments, nil } // Check for data.attachments if data, ok := mapResult["data"].(map[string]interface{}); ok { if attList, ok := data["attachments"].([]interface{}); ok { log.Printf("GetJobAttachments: Found %d attachments in 'data.attachments' field for job %s", len(attList), jobID) for _, att := range attList { if attachment, ok := att.(map[string]interface{}); ok { attachments = append(attachments, attachment) } } return attachments, nil } } // If nothing found, return empty with a warning log.Printf("GetJobAttachments: No attachments found in response for job %s: %s", jobID, string(body)) return attachments, nil } if len(result.Attachments) == 0 { log.Printf("GetJobAttachments: No attachments found for job %s in standard format", jobID) } else { log.Printf("GetJobAttachments: Found %d attachments for job %s in standard format", len(result.Attachments), jobID) } return result.Attachments, nil }