|  |  | @ -1,8 +1,12 @@ | 
			
		
	
		
		
			
				
					
					|  |  |  | # frozen_string_literal: true |  |  |  | # frozen_string_literal: true | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | require 'tempfile' | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | require 'base64' | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | module Api |  |  |  | module Api | 
			
		
	
		
		
			
				
					
					|  |  |  |   class TemplatesController < ApiBaseController |  |  |  |   class TemplatesController < ApiBaseController | 
			
		
	
		
		
			
				
					
					|  |  |  |     load_and_authorize_resource :template |  |  |  |     skip_authorization_check | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     load_and_authorize_resource :template, except: [:pdf] | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     def index |  |  |  |     def index | 
			
		
	
		
		
			
				
					
					|  |  |  |       templates = filter_templates(@templates, params) |  |  |  |       templates = filter_templates(@templates, params) | 
			
		
	
	
		
		
			
				
					|  |  | @ -85,8 +89,132 @@ module Api | 
			
		
	
		
		
			
				
					
					|  |  |  |       render json: @template.as_json(only: %i[id archived_at]) |  |  |  |       render json: @template.as_json(only: %i[id archived_at]) | 
			
		
	
		
		
			
				
					
					|  |  |  |     end |  |  |  |     end | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     def pdf | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       template = Template.new | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       template.account = current_account | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       template.author = current_user | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       template.folder = TemplateFolders.find_or_create_by_name(current_user, params[:folder_name]) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       template.name = params[:name] || 'Untitled Template' | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       template.external_id = params[:external_id] if params[:external_id].present? | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       template.source = :api | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       template.save! | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             begin | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Starting document processing..." | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         documents = process_documents(template, params[:documents]) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Documents processed: #{documents.count}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         schema = documents.map { |doc| { attachment_uuid: doc.uuid, name: doc.filename.base } } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Schema created: #{schema}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         if template.fields.blank? | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           puts "DEBUG: Processing fields..." | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           template.fields = Templates::ProcessDocument.normalize_attachment_fields(template, documents) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           schema.each { |item| item['pending_fields'] = true } if template.fields.present? | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           puts "DEBUG: Fields processed: #{template.fields.count}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         end | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Updating template schema..." | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         template.update!(schema: schema) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Template schema updated" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Enqueueing webhooks..." | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         enqueue_template_created_webhooks(template) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Webhooks enqueued" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Enqueueing search reindex..." | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         SearchEntries.enqueue_reindex(template) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Search reindex enqueued" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Get the documents for serialization | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Getting template documents for serialization..." | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         template_documents = template.documents.where(uuid: documents.map(&:uuid)) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Template documents found: #{template_documents.count}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Serializing template..." | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         result = Templates::SerializeForApi.call(template, template_documents) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Template serialized successfully" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         render json: result | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       rescue StandardError => e | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: ERROR OCCURRED: #{e.class} - #{e.message}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Backtrace: #{e.backtrace.first(5).join("\n")}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         template.destroy! | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         raise e | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       end | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     rescue Templates::CreateAttachments::PdfEncrypted | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       render json: { error: 'PDF encrypted', status: 'pdf_encrypted' }, status: :unprocessable_entity | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     rescue StandardError => e | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       Rollbar.error(e) if defined?(Rollbar) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       render json: { error: 'Unable to create template' }, status: :unprocessable_entity | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     end | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     private |  |  |  |     private | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         def process_documents(template, documents_params) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       puts "DEBUG: process_documents called with #{documents_params&.count || 0} documents" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       return [] if documents_params.blank? | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       documents_params.map.with_index do |doc_param, index| | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Processing document #{index + 1}: #{doc_param[:name]}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Base64 string length: #{doc_param[:file].length}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Base64 string first 100 chars: #{doc_param[:file][0..99]}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Base64 string last 100 chars: #{doc_param[:file][-100..-1]}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Check if the base64 string looks truncated | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         expected_length = (doc_param[:file].length / 4.0 * 3).ceil | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Expected decoded size: #{expected_length} bytes" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Base64 string ends with padding: #{doc_param[:file].end_with?('==') || doc_param[:file].end_with?('=')}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Base64 string length is multiple of 4: #{doc_param[:file].length % 4 == 0}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Validate base64 string | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         unless doc_param[:file].match?(/\A[A-Za-z0-9+\/]*={0,2}\z/) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           puts "DEBUG: ERROR: Invalid base64 string format" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           raise ArgumentError, "Invalid base64 string format" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         end | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Decode base64 file data | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         file_data = Base64.decode64(doc_param[:file]) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: File data decoded, size: #{file_data.size} bytes" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: First 50 bytes as hex: #{file_data[0..49].unpack('H*').first}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: First 50 bytes as string: #{file_data[0..49].inspect}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Check if the decoded data looks like a PDF | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         if file_data.size >= 4 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           pdf_header = file_data[0..3] | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           puts "DEBUG: PDF header: #{pdf_header.inspect}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           puts "DEBUG: Is PDF header valid: #{pdf_header == '%PDF'}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         end | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Create a temporary file-like object | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         file = Tempfile.new(['document', '.pdf']) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         file.binmode | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         file.write(file_data) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         file.rewind | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Add original filename | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         file.define_singleton_method(:original_filename) { doc_param[:name] } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         file.define_singleton_method(:content_type) { 'application/pdf' } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Calling Templates::CreateAttachments.handle_pdf_or_image..." | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         # Process the document using the existing service | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         result = Templates::CreateAttachments.handle_pdf_or_image(template, file, file_data, {}, extract_fields: true) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         puts "DEBUG: Document processed successfully: #{result.class}" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         result | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       ensure | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         file&.close | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         file&.unlink | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       end | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     end | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     def enqueue_template_created_webhooks(template) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       WebhookUrls.for_account_id(template.account_id, 'template.created').each do |webhook_url| | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         SendTemplateCreatedWebhookRequestJob.perform_async('template_id' => template.id, | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                                                            'webhook_url_id' => webhook_url.id) | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       end | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     end | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     def filter_templates(templates, params) |  |  |  |     def filter_templates(templates, params) | 
			
		
	
		
		
			
				
					
					|  |  |  |       templates = Templates.search(current_user, templates, params[:q]) |  |  |  |       templates = Templates.search(current_user, templates, params[:q]) | 
			
		
	
		
		
			
				
					
					|  |  |  |       templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active |  |  |  |       templates = params[:archived].in?(['true', true]) ? templates.archived : templates.active | 
			
		
	
	
		
		
			
				
					|  |  | 
 |