Class: Edi::Amazon::APlusContentProcessor

Inherits:
BaseEdiService show all
Defined in:
app/services/edi/amazon/a_plus_content_processor.rb

Overview

Processes and copies Amazon A+ Content documents across marketplaces.

Reads published A+ content from EDI communication logs, persists it as
AmazonAPlusContent records, and can copy existing content documents
(including their images) to a new marketplace/locale.

Constant Summary

Constants included from RequestIdentifiable

RequestIdentifiable::REQUEST_ID_HEADERS

Constants included from Edi::AddressAbbreviator

Edi::AddressAbbreviator::MAX_LENGTH

Instance Attribute Summary

Attributes inherited from BaseEdiService

#orchestrator

Attributes inherited from BaseService

#options

Instance Method Summary collapse

Methods inherited from BaseEdiService

#amazon_feed_product_type, #duplicate_po_already_notified?, #initialize, #mark_duplicate_po_as_notified, #onboard_ordered_catalog_items, #report_order_creation_issues, #safe_process_edi_communication_log

Methods included from RequestIdentifiable

#partner_request_id

Methods included from Edi::AddressAbbreviator

#abbreviate_street, #collect_street_originals, #record_address_abbreviation_notes

Methods inherited from BaseService

#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #tagged_logger

Constructor Details

This class inherits a constructor from Edi::BaseEdiService

Instance Method Details

#compress_image(image_data, max_size) ⇒ String?

Compresses image binary data so it fits within a byte limit.

Parameters:

  • image_data (String)

    raw image bytes

  • max_size (Integer)

    maximum allowed size in bytes

Returns:

  • (String, nil)

    compressed image bytes, or nil if compression fails



166
167
168
169
170
171
172
173
174
# File 'app/services/edi/amazon/a_plus_content_processor.rb', line 166

def compress_image(image_data, max_size)
  # Use Vips to compress the image to fit within the size limit
  compressed_data = compress_image_with_vips(image_data, max_size)
  return compressed_data if compressed_data

  # If Vips fails, we can't proceed
  Rails.logger.error 'Failed to compress image to acceptable size'
  nil
end

#compress_image_with_vips(image_data, max_size) ⇒ String?

Compresses image binary data using libvips.

Resizes very wide images and iteratively lowers JPEG quality until the
output fits within max_size or the attempt limit is reached.

Parameters:

  • image_data (String)

    raw image bytes

  • max_size (Integer)

    maximum allowed size in bytes

Returns:

  • (String, nil)

    compressed image bytes, or nil if compression fails



184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'app/services/edi/amazon/a_plus_content_processor.rb', line 184

def compress_image_with_vips(image_data, max_size)
  # Compress image using Vips
  require 'vips'

  begin
    # Create a temporary file from the image data
    temp_file = Tempfile.new(['image', '.jpg'])
    temp_file.binmode
    temp_file.write(image_data)
    temp_file.close

    # Load with Vips
    image = Vips::Image.new_from_file(temp_file.path)

    # Resize if the image is very large (width > 1500px)
    if image.width > 1500
      scale = 1500.0 / image.width
      image = image.resize(scale)
    end

    # Start with quality 95 and reduce if needed
    quality = 95
    max_attempts = 8

    max_attempts.times do |attempt|
      # Create compressed version
      compressed_file = Tempfile.new(['compressed', '.jpg'])
      compressed_file.close

      begin
        # Write as JPEG with current quality
        image.write_to_file(compressed_file.path, Q: quality, strip: true)

        # Check the size
        compressed_size = File.size(compressed_file.path)

        if compressed_size <= max_size
          # Success! Read the compressed data
          compressed_data = File.binread(compressed_file.path)
          compressed_file.unlink
          temp_file.unlink
          return compressed_data
        else
          # Reduce quality and try again
          quality = (quality * 0.75).to_i
          quality = [quality, 20].max # Don't go below 20% quality
          compressed_file.unlink
        end
      rescue StandardError => e
        Rails.logger.error "Error during image compression attempt #{attempt + 1}: #{e.message}"
        compressed_file&.unlink
        quality = (quality * 0.75).to_i
      end
    end

    # If we get here, compression failed
    temp_file.unlink
    Rails.logger.error "Failed to compress image to acceptable size after #{max_attempts} attempts"
    nil
  rescue StandardError => e
    Rails.logger.error "Error compressing image with Vips: #{e.message}"
    temp_file&.unlink
    nil
  end
end

#copy_content_to_destination(source_content, target_marketplace_id:, target_locale:) ⇒ Boolean

Copies an existing A+ content document to a new marketplace and locale.

Fetches the source document, uploads its images, validates the document,
and creates the new document in the target marketplace.

Parameters:

  • source_content (AmazonAPlusContent)

    source A+ content record

  • target_marketplace_id (String)

    target Amazon marketplace identifier

  • target_locale (String)

    target locale (e.g. en_CA)

Returns:

  • (Boolean)

    true when the copy succeeds, false otherwise



86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'app/services/edi/amazon/a_plus_content_processor.rb', line 86

def copy_content_to_destination(source_content, target_marketplace_id:, target_locale:)
  Rails.logger.info "Starting A+ content copy process for content reference key: #{source_content.content_reference_key}"

  @error_messages = [] # Initialize error collection

  begin
    # Step 1: Fetch the original content document using contentReferenceKey
    source_marketplace_id = source_content.marketplace_id || orchestrator.marketplace
    content_document = fetch_content_document(source_content.content_reference_key, source_marketplace_id)
    unless content_document
      error_msg = "Failed to fetch content document for key: #{source_content.content_reference_key}"
      @error_messages << error_msg
      raise StandardError, error_msg
    end

    # Step 2: Create upload destinations for any images in the content
    upload_destinations = create_upload_destinations_for_images(content_document, target_marketplace_id)
    # Check if upload_destinations is nil (failed) or empty (no images found)
    if upload_destinations.nil?
      error_msg = 'Failed to create upload destinations for images'
      @error_messages << error_msg
      raise StandardError, error_msg
    elsif upload_destinations.empty?
      Rails.logger.info 'No images found in content document, skipping image upload step'
    end

    # Step 3: Upload images (if any)
    if upload_destinations.empty?
      Rails.logger.info 'Skipping image upload step - no images to upload'
    else
      unless upload_images(content_document, upload_destinations)
        error_msg = 'Failed to upload images for content copy'
        @error_messages << error_msg
        raise StandardError, error_msg
      end
    end

    # Step 4: Validate the content document before creating it
    unless validate_content_document(content_document, target_marketplace_id)
      error_msg = 'Content document validation failed'
      @error_messages << error_msg
      raise StandardError, error_msg
    end

    # Step 5: Create the new content document
    new_content_reference_key = create_content_document(content_document, target_marketplace_id, target_locale)
    unless new_content_reference_key
      error_msg = 'Failed to create new content document'
      @error_messages << error_msg
      raise StandardError, error_msg
    end

    # Step 6: Add ASINs to the content document
    # SKIP FOR NOW
    # target_asin = source_content.asin
    # raise StandardError, 'Failed to add ASINs to content document' unless add_asins_to_content_document(new_content_reference_key, target_marketplace_id, [target_asin])

    # Rails.logger.info "Successfully copied A+ content. New content reference key: #{new_content_reference_key}"
    true
  rescue StandardError => e
    error_msg = "Error during A+ content copy: #{e.message}"
    @error_messages << error_msg
    Rails.logger.error error_msg
    Rails.logger.error e.backtrace.join("\n")
    false
  end
end

#error_messagesArray<String>

Returns error messages accumulated during the last copy attempt.

Returns:

  • (Array<String>)

    collected error messages



157
158
159
# File 'app/services/edi/amazon/a_plus_content_processor.rb', line 157

def error_messages
  @error_messages || []
end

#process(ecl, catalog_item: nil, locale: nil) ⇒ Boolean

Processes an EDI communication log containing published A+ content records.

Parameters:

  • ecl (EdiCommunicationLog)

    inbound EDI log with A+ content JSON

  • catalog_item (CatalogItem, nil) (defaults to: nil)

    catalog item to associate (resolved from ECL if omitted)

  • locale (String, nil) (defaults to: nil)

    Amazon locale (defaults to en_US)

Returns:

  • (Boolean)

    true if any A+ content records were saved



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'app/services/edi/amazon/a_plus_content_processor.rb', line 21

def process(ecl, catalog_item: nil, locale: nil)
  res = false

  if ecl&.data.present? && (json_hash = JSON.parse(ecl.data).with_indifferent_access).present?
    Rails.logger.debug { "Edi::Amazon::APlusContentProcessor#process, json_hash: #{json_hash}" }

    # Find the catalog item if we don't have it from the arguments
    asin = ecl.file_info[:asin]
    catalog_item ||= ecl.orchestrator.customer.catalog.catalog_items.amazons_with_asins.find_by(items: { amazon_asin: asin })

    return false unless catalog_item

    # Get marketplace info
    marketplace_id = orchestrator.marketplace
    marketplace = catalog_item.amazon_marketplace

    # Process publish records
    publish_records = json_hash['publishRecordList'] || []

    publish_records.each do |record|
      next if record['contentReferenceKey'].blank?

      # Use the ASIN from the record, not from file_info
      record_asin = record['asin']

      # Find or create the A+ content record
      a_plus_content = AmazonAPlusContent.find_or_initialize_by(
        asin: record_asin,
        marketplace_id: marketplace_id,
        locale: locale || 'en_US'
      )

      # Update with simplified data
      a_plus_content.assign_attributes(
        catalog_item: catalog_item,
        amazon_marketplace: marketplace,
        content_reference_key: record['contentReferenceKey'],
        content_type: record['contentType'],
        content_sub_type: record['contentSubType'],
        page_name: record['contentType'], # Use content type as page name for now
        content_data: json_hash, # Keep the full response for reference
        status: 'approved' # Assume approved since it's published
      )

      if a_plus_content.save
        res = true
        Rails.logger.info "Successfully saved A+ content for ASIN #{asin}, content reference key: #{record['contentReferenceKey']}"
      else
        Rails.logger.error "Failed to save A+ content for ASIN #{asin}: #{a_plus_content.errors.full_messages.join(', ')}"
      end
    end
  end

  res
end