Class: Feed

Inherits:
ApplicationRecord show all
Includes:
ERB::Util, Models::Auditable
Defined in:
app/models/feed.rb

Overview

== Schema Information

Table name: feeds
Database name: primary

id :integer not null, primary key
active :boolean default(TRUE), not null
cc_email :string
contact_info :text
custom1 :string(255)
custom2 :string(255)
custom3 :string(255)
custom_file_formatter :string
destination :enum default("ftp")
emails :string default([]), is an Array
encoding :string(25) default("UTF-8")
feed_type :enum default("inventory")
file_name_template :string(255)
frequency :enum default("hourly")
ftp_passive :boolean default(FALSE), not null
hostname :string(255)
name :string(255) not null
new_line_method :enum default("crlf")
next_attempt :datetime
output_format :enum default("txt")
output_layout :text not null
password :string(255)
remote_directory :string(255)
sender_email :string
username :string(255)
created_at :datetime
updated_at :datetime
creator_id :integer
customer_id :integer not null
updater_id :integer

Indexes

feeds_customer_id_idx (customer_id)

Foreign Keys

feeds_customer_id_fk (customer_id => parties.id) ON DELETE => cascade

DEPRECATED / dormant. The reseller-inventory and OpenAI product feeds were
converged onto Edi::ResellerInventory and Edi::Openai (EdiCommunicationLog) — see
doc/tasks/202606190800_OPENAI_ADS_EDI_CONVERGENCE.md (PR #1219). This PR removed
the legacy runtime (FeedRunnerWorker, the scheduled process_feeds driver, the CRM
feeds UI). The model and feeds table are kept for now as the Zeitwerk namespace
anchor for Feed::Google::* / Feed::OpenaiAds::* and to preserve history; the
remaining transmit/render instance methods are no longer invoked and are stripped
together with the table drop in the stage-2 cleanup once the deprecation window is
clear.

Defined Under Namespace

Classes: ItemBaseGenerator, ItemInventoryGenerator, ItemListGenerator, ItemPresenter, ProductTypeGenerator, TransmitEmail, TransmitFtp, TransmitFtps, TransmitSftp

Constant Summary

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Delegated Instance Attributes collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has many collapse

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#custom_file_formatterObject (readonly)



68
# File 'app/models/feed.rb', line 68

validates :custom_file_formatter, presence: { if: :custom_formatter? }

#destinationObject (readonly)



66
# File 'app/models/feed.rb', line 66

validates :name, :feed_type, :destination, presence: true

#encodingObject (readonly)



71
# File 'app/models/feed.rb', line 71

validates :encoding, presence: true

#feed_typeObject (readonly)



66
# File 'app/models/feed.rb', line 66

validates :name, :feed_type, :destination, presence: true

#hostnameObject (readonly)



69
# File 'app/models/feed.rb', line 69

validates :hostname, :username, :password, presence: { if: :ftp? }

#nameObject (readonly)



66
# File 'app/models/feed.rb', line 66

validates :name, :feed_type, :destination, presence: true

#output_formatObject (readonly)



67
# File 'app/models/feed.rb', line 67

validates :output_format, :output_layout, presence: { if: :requires_output_format? }

#output_layoutObject (readonly)



67
# File 'app/models/feed.rb', line 67

validates :output_format, :output_layout, presence: { if: :requires_output_format? }

#passwordObject (readonly)



69
# File 'app/models/feed.rb', line 69

validates :hostname, :username, :password, presence: { if: :ftp? }

#sender_emailObject (readonly)



70
# File 'app/models/feed.rb', line 70

validates :sender_email, presence: { if: :email? }

#usernameObject (readonly)



69
# File 'app/models/feed.rb', line 69

validates :hostname, :username, :password, presence: { if: :ftp? }

Class Method Details

.activeActiveRecord::Relation<Feed>

A relation of Feeds that are active. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Feed>)

See Also:



78
# File 'app/models/feed.rb', line 78

scope :active, -> { where(active: true) }

.custom_file_formatter_for_selectObject



117
118
119
# File 'app/models/feed.rb', line 117

def self.custom_file_formatter_for_select
  %w[]
end

.destinations_for_selectObject



109
110
111
# File 'app/models/feed.rb', line 109

def self.destinations_for_select
  destinations.keys
end

.encoding_list_for_selectObject



208
209
210
# File 'app/models/feed.rb', line 208

def self.encoding_list_for_select
  Encoding.list.map(&:to_s)
end

.feed_types_for_selectObject



101
102
103
# File 'app/models/feed.rb', line 101

def self.feed_types_for_select
  feed_types.keys
end

.frequencies_for_selectObject



113
114
115
# File 'app/models/feed.rb', line 113

def self.frequencies_for_select
  frequencies.keys
end

.output_formats_for_selectObject



105
106
107
# File 'app/models/feed.rb', line 105

def self.output_formats_for_select
  output_formats.keys
end

.timedActiveRecord::Relation<Feed>

A relation of Feeds that are timed. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Feed>)

See Also:



79
# File 'app/models/feed.rb', line 79

scope :timed, -> { where.not(frequency: nil) }

Instance Method Details

#all_to_emailsObject



131
132
133
# File 'app/models/feed.rb', line 131

def all_to_emails
  contact_points.map(&:detail) + (emails || [])
end

#build_transmitterObject

Builds a transmitter for ftp or email destination



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'app/models/feed.rb', line 259

def build_transmitter
  case destination.to_sym
  when :ftp
    transmitter = Feed::TransmitFtp.new(
      logger:,
      hostname:,
      ftp_passive:,
      username:,
      password:,
      remote_directory:,
      feed_id: id
    )
  when :ftps
    transmitter = Feed::TransmitFtps.new(
      logger:,
      hostname:,
      ftp_passive:,
      username:,
      password:,
      remote_directory:,
      feed_id: id
    )
  when :sftp
    transmitter = Feed::TransmitSftp.new(
      logger:,
      hostname:,
      username:,
      password:,
      remote_directory:,
      feed_id: id
    )
  when :email
    transmitter = Feed::TransmitEmail.new(
      logger:,
      recipient_contact_points: contact_points,
      sender_email:,
      cc_email:,
      extra_emails: emails,
      feed_id: id
    )
  else
    raise 'Unsupported Destination, cannot resolve Transmitter Type'
  end
  transmitter
end

#calculate_next_attemptObject



229
230
231
232
233
234
235
236
237
238
239
# File 'app/models/feed.rb', line 229

def calculate_next_attempt
  # Set next attempt based on frequency
  case frequency
  when 'hourly'
    1.hour.from_now
  when 'weekly'
    1.week.from_now
  else
    1.day.from_now
  end
end

#catalogObject



151
152
153
# File 'app/models/feed.rb', line 151

def catalog
  customer&.catalog
end

#catalog_itemsObject



155
156
157
158
159
160
161
# File 'app/models/feed.rb', line 155

def catalog_items
  return CatalogItem.none unless catalog

  catalog_item_query = catalog.catalog_items.for_edi_feeds
  catalog_item_query = catalog_item_query.where('third_party_sku ~ ?', catalog.third_party_sku_filter_regex) if catalog.is_active_third_party_sku_filter
  catalog_item_query
end

#catalog_items_sizeObject

Alias for Catalog_items#size

Returns:

  • (Object)

    Catalog_items#catalog_items_size

See Also:



60
# File 'app/models/feed.rb', line 60

delegate :size, to: :catalog_items, prefix: true

#contact_pointsActiveRecord::Relation<ContactPoint>

Returns:

See Also:



64
# File 'app/models/feed.rb', line 64

has_and_belongs_to_many :contact_points, inverse_of: :feeds

#contact_points_for_selectObject



121
122
123
# File 'app/models/feed.rb', line 121

def contact_points_for_select
  customer.all_contact_points.emails.map { |cp| ["#{cp.party.full_name} - #{cp.detail}", cp.id] }.sort_by { |cp| cp[0] }
end

#custom_formatter_classObject

Raises:

  • (NameError)


178
179
180
181
182
183
# File 'app/models/feed.rb', line 178

def custom_formatter_class
  klass = "Edi::FileFormat::#{custom_file_formatter}".safe_constantize
  raise NameError, "Custom file formatter class Edi::FileFormat::#{custom_file_formatter} not found for Feed ##{id}" unless klass

  klass
end

#customerCustomer

Returns:

See Also:



62
# File 'app/models/feed.rb', line 62

belongs_to :customer, inverse_of: :feeds, optional: true

#deep_dupObject



92
93
94
95
96
97
98
99
# File 'app/models/feed.rb', line 92

def deep_dup
  deep_clone do |original, copy|
    if copy.is_a?(Feed)
      copy.name = "#{original.name} (Copy)"
      copy.active = false
    end
  end
end

#encoding_to_useObject



198
199
200
201
202
203
204
205
206
# File 'app/models/feed.rb', line 198

def encoding_to_use
  enc = begin
    Encoding.find(encoding)
  rescue StandardError
    nil
  end
  enc ||= Encoding.find('UTF-8')
  enc.to_s
end

#feed_attributesObject

Returns relevant instance variables to be used in template merge



141
142
143
144
145
146
147
148
149
# File 'app/models/feed.rb', line 141

def feed_attributes
  res = {}
  case feed_type.to_sym
  when :inventory
    res[:catalog_items] = catalog_items
  end
  res[:unique_id] = Time.current.to_i
  res
end

#feed_historiesActiveRecord::Relation<FeedHistory>

Returns:

See Also:



63
# File 'app/models/feed.rb', line 63

has_many :feed_histories, inverse_of: :feed

#file_nameObject



216
217
218
219
220
221
222
223
224
225
226
227
# File 'app/models/feed.rb', line 216

def file_name
  @file_name ||= ERB.new(file_name_template).result(binding) if file_name_template.present?
  if @file_name.nil? && custom_formatter?
    begin
      @file_name = custom_formatter_class.file_name
    rescue NameError => e
      Rails.logger.error("[Feed#file_name] #{e.message} — falling back to default name")
    end
  end
  @file_name ||= "#{feed_type}_#{id}_#{customer_id}_#{Time.current.strftime('%y%m%d%H%M')}.#{output_format}"
  @file_name
end

#last_feedObject



332
333
334
# File 'app/models/feed.rb', line 332

def last_feed
  feed_histories.order(id: :desc).try(:first)
end

#last_run_dateObject



336
337
338
# File 'app/models/feed.rb', line 336

def last_run_date
  last_feed.try(:created_at)
end

#last_run_resultObject



340
341
342
# File 'app/models/feed.rb', line 340

def last_run_result
  last_feed.try(:result)
end

#output_to_fileObject



305
306
307
308
309
# File 'app/models/feed.rb', line 305

def output_to_file
  local_file_path = Rails.application.config.x.temp_storage_path.join(file_name)
  render_to_file local_file_path
  local_file_path
end

#renderObject



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'app/models/feed.rb', line 163

def render
  # Create some bindings for specific types
  if product_data?
    res = Feed::ItemListGenerator.new.process
    output = res.output
  else # ERB style render
    @date = Time.current
    feed_attributes.each { |k, v| instance_variable_set(:"@#{k}", v) }
    @feed = self
    output = ERB.new(output_layout).result(binding)
  end
  output = output.gsub("\r\n", "\n") if universal?
  output.encode(encoding_to_use, invalid: :replace, undef: :replace)
end

#render_to_file(file_path) ⇒ Object



185
186
187
188
189
190
191
192
193
194
195
196
# File 'app/models/feed.rb', line 185

def render_to_file(file_path)
  if custom_formatter?
    formatter = custom_formatter_class.new(output_location: file_path)
    formatter.process feed_attributes
  else
    File.open(file_path, "w+:#{encoding_to_use}") do |f|
      f.write(render)
      f.flush
      f.fsync
    end
  end
end

#runObject



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'app/models/feed.rb', line 241

def run
  return unless active

  # Only one method for now
  Rails.logger.info "Running feed #{id}"
  res = transmit

  if res.transmitted?
    update_column(:next_attempt, calculate_next_attempt) # Don't require validation
  else
    Rails.logger.error " ** Error running feed #{id} #{res.message}"
    update_column(:next_attempt, 1.hour.from_now) # Don't require validation
  end
  Rails.logger.info "Feed id #{id} complete, next run scheduled for #{next_attempt}"
  res
end

#sender_email_options_for_selectObject



125
126
127
128
129
# File 'app/models/feed.rb', line 125

def sender_email_options_for_select
  ['ediadmin@warmlyyours.com'] +
    ['orders@warmlyyours.com'] +
    Employee.active_employees.pluck(:email).sort
end

#ssc(str) ⇒ Object

Strip Special Characters



136
137
138
# File 'app/models/feed.rb', line 136

def ssc(str)
  str.gsub(%r{[^0-9A-Za-z.-/]}, ' ').squish
end

#to_sObject



212
213
214
# File 'app/models/feed.rb', line 212

def to_s
  "Feed #{id}"
end

#transmitObject



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'app/models/feed.rb', line 311

def transmit
  local_file_path = output_to_file

  raise "Feed #{local_file_path} not found" unless File.exist? local_file_path

  upload = Upload.uploadify(local_file_path.to_s, 'Feed')
  logger.info "Job output uploaded to upload id #{upload.id}"

  transmitter = build_transmitter

  res = transmitter.process(upload)

  feed_histories.create(result: res.status,
                             file_name: upload.try(:attachment_name),
                             notes: res.message,
                             communication: res.try(:communication),
                             upload:)

  res
end